Merge "Adding ktlint hook to support library." into oc-mr1-support-27.0-dev
am: ee4da728f2
Change-Id: Ia53c99b17a6f9a59bfe892982f9147ad75611157
diff --git a/annotations/src/main/java/android/support/annotation/AnyThread.java b/annotations/src/main/java/android/support/annotation/AnyThread.java
index 4c379d3..b006922 100644
--- a/annotations/src/main/java/android/support/annotation/AnyThread.java
+++ b/annotations/src/main/java/android/support/annotation/AnyThread.java
@@ -17,6 +17,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -41,6 +42,6 @@
*/
@Documented
@Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
public @interface AnyThread {
}
diff --git a/annotations/src/main/java/android/support/annotation/BinderThread.java b/annotations/src/main/java/android/support/annotation/BinderThread.java
index 0b821d5..5d9a3c2 100644
--- a/annotations/src/main/java/android/support/annotation/BinderThread.java
+++ b/annotations/src/main/java/android/support/annotation/BinderThread.java
@@ -17,6 +17,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -37,6 +38,6 @@
*/
@Documented
@Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
public @interface BinderThread {
}
\ No newline at end of file
diff --git a/annotations/src/main/java/android/support/annotation/MainThread.java b/annotations/src/main/java/android/support/annotation/MainThread.java
index 2f50306..78541d5 100644
--- a/annotations/src/main/java/android/support/annotation/MainThread.java
+++ b/annotations/src/main/java/android/support/annotation/MainThread.java
@@ -17,6 +17,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -45,6 +46,6 @@
*/
@Documented
@Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
public @interface MainThread {
}
diff --git a/annotations/src/main/java/android/support/annotation/UiThread.java b/annotations/src/main/java/android/support/annotation/UiThread.java
index 0a9a0c1..1d7aeca 100644
--- a/annotations/src/main/java/android/support/annotation/UiThread.java
+++ b/annotations/src/main/java/android/support/annotation/UiThread.java
@@ -17,6 +17,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -46,6 +47,6 @@
*/
@Documented
@Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
public @interface UiThread {
}
diff --git a/annotations/src/main/java/android/support/annotation/WorkerThread.java b/annotations/src/main/java/android/support/annotation/WorkerThread.java
index 237aa66..8b08b14 100644
--- a/annotations/src/main/java/android/support/annotation/WorkerThread.java
+++ b/annotations/src/main/java/android/support/annotation/WorkerThread.java
@@ -17,6 +17,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -37,6 +38,6 @@
*/
@Documented
@Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
public @interface WorkerThread {
}
\ No newline at end of file
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index 6b613d0..ccbd35a 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -150,8 +150,7 @@
// Only modify Android projects.
if (project.name.equals('doclava')
|| project.name.equals('jdiff')
- || project.name.equals('noto-emoji-compat')
- || project.name.equals('support-media-compat-test-lib')) {
+ || project.name.equals('noto-emoji-compat')) {
// disable tests and return
project.tasks.whenTaskAdded { task ->
if (task instanceof org.gradle.api.tasks.testing.Test) {
diff --git a/buildSrc/src/main/java/android/support/LibraryVersions.java b/buildSrc/src/main/java/android/support/LibraryVersions.java
index 3f160df..27a52bd 100644
--- a/buildSrc/src/main/java/android/support/LibraryVersions.java
+++ b/buildSrc/src/main/java/android/support/LibraryVersions.java
@@ -23,7 +23,7 @@
/**
* Version code of the support library components.
*/
- public static final Version SUPPORT_LIBRARY = new Version("27.0.2");
+ public static final Version SUPPORT_LIBRARY = new Version("27.1.0-SNAPSHOT");
/**
* Version code for flatfoot 1.0 projects (room, lifecycles)
diff --git a/car/Android.mk b/car/Android.mk
new file mode 100644
index 0000000..fa20f26
--- /dev/null
+++ b/car/Android.mk
@@ -0,0 +1,39 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Here is the final static library that apps can link against.
+# Applications that use this library must specify
+#
+# LOCAL_STATIC_ANDROID_LIBRARIES := \
+# android-support-car
+#
+# in their makefiles to include the resources and their dependencies in their package.
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE := android-support-car
+LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
+LOCAL_SRC_FILES := $(call all-java-files-under,src/main/java)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_SHARED_ANDROID_LIBRARIES := \
+ android-support-annotations \
+ android-support-v4 \
+ android-support-v7-appcompat \
+ android-support-v7-cardview \
+ android-support-v7-recyclerview
+LOCAL_JAR_EXCLUDE_FILES := none
+LOCAL_JAVA_LANGUAGE_VERSION := 1.8
+LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/car/AndroidManifest.xml b/car/AndroidManifest.xml
new file mode 100644
index 0000000..4e6d80f
--- /dev/null
+++ b/car/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.car">
+</manifest>
diff --git a/car/OWNERS b/car/OWNERS
new file mode 100644
index 0000000..d226975
--- /dev/null
+++ b/car/OWNERS
@@ -0,0 +1 @@
+ajchen@google.com
\ No newline at end of file
diff --git a/car/README.txt b/car/README.txt
new file mode 100644
index 0000000..50a019b
--- /dev/null
+++ b/car/README.txt
@@ -0,0 +1 @@
+Library Project including Car Support UI Components and associated utilities.
diff --git a/car/build.gradle b/car/build.gradle
new file mode 100644
index 0000000..c1be3a3
--- /dev/null
+++ b/car/build.gradle
@@ -0,0 +1,39 @@
+import android.support.LibraryGroups
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api project(':appcompat-v7')
+ api project(':cardview-v7')
+ api project(':support-annotations')
+ api project(':support-v4')
+ api project(':recyclerview-v7')
+
+ androidTestImplementation libs.test_runner, { exclude module: 'support-annotations' }
+ androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
+ androidTestImplementation libs.espresso_contrib, { exclude group: 'com.android.support' }
+ androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+ androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 24
+ }
+
+ sourceSets {
+ main.res.srcDirs 'res', 'res-public'
+ }
+}
+
+supportLibrary {
+ name = "Android Car Support UI"
+ publish = false
+ mavenGroup = LibraryGroups.SUPPORT
+ inceptionYear = "2017"
+ description = "Android Car Support UI"
+ java8Library = true
+ legacySourceLocation = true
+}
diff --git a/media-compat-test-client/lint-baseline.xml b/car/lint-baseline.xml
similarity index 100%
copy from media-compat-test-client/lint-baseline.xml
copy to car/lint-baseline.xml
diff --git a/car/res-public/values/public_attrs.xml b/car/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..5351366
--- /dev/null
+++ b/car/res-public/values/public_attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Definitions of attributes to be exposed as public. -->
+<resources>
+ <!-- ColumnCardView -->
+ <public type="attr" name="columnSpan" />
+</resources>
diff --git a/car/res/anim/fade_in_trans_left.xml b/car/res/anim/fade_in_trans_left.xml
new file mode 100644
index 0000000..2d6bab5
--- /dev/null
+++ b/car/res/anim/fade_in_trans_left.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime">
+ <translate
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:fromXDelta="-10%p"
+ android:toXDelta="0" />
+
+ <alpha
+ android:fromAlpha="0.2"
+ android:toAlpha="1"
+ android:interpolator="@android:interpolator/decelerate_quint" />
+</set>
diff --git a/car/res/anim/fade_in_trans_left_layout_anim.xml b/car/res/anim/fade_in_trans_left_layout_anim.xml
new file mode 100644
index 0000000..e7660db
--- /dev/null
+++ b/car/res/anim/fade_in_trans_left_layout_anim.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<layoutAnimation
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:animation="@anim/fade_in_trans_left"
+ android:delay="0%"
+ android:animationOrder="normal" />
diff --git a/car/res/anim/fade_in_trans_right.xml b/car/res/anim/fade_in_trans_right.xml
new file mode 100644
index 0000000..5cbeb59
--- /dev/null
+++ b/car/res/anim/fade_in_trans_right.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime">
+ <translate
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:fromXDelta="10%p"
+ android:toXDelta="0" />
+
+ <alpha
+ android:fromAlpha="0.2"
+ android:toAlpha="1"
+ android:interpolator="@android:interpolator/decelerate_quint" />
+</set>
diff --git a/car/res/anim/fade_in_trans_right_layout_anim.xml b/car/res/anim/fade_in_trans_right_layout_anim.xml
new file mode 100644
index 0000000..b76de23
--- /dev/null
+++ b/car/res/anim/fade_in_trans_right_layout_anim.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<layoutAnimation
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:animation="@anim/fade_in_trans_right"
+ android:delay="0%"
+ android:animationOrder="normal" />
diff --git a/car/res/drawable/car_button_background.xml b/car/res/drawable/car_button_background.xml
new file mode 100644
index 0000000..3b139d9
--- /dev/null
+++ b/car/res/drawable/car_button_background.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Default background styles for car buttons when enabled/disabled. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_button_radius" />
+ <solid android:color="@color/car_grey_300"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_button_radius" />
+ <solid android:color="@color/car_highlight"/>
+ </shape>
+ </item>
+</selector>
diff --git a/car/res/drawable/car_button_text_color.xml b/car/res/drawable/car_button_text_color.xml
new file mode 100644
index 0000000..b14ec68
--- /dev/null
+++ b/car/res/drawable/car_button_text_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Default text colors for car buttons when enabled/disabled. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:color="@color/car_grey_700" />
+ <item android:color="@color/car_action1"/>
+</selector>
diff --git a/car/res/drawable/car_drawer_list_item_background.xml b/car/res/drawable/car_drawer_list_item_background.xml
new file mode 100644
index 0000000..c5fc36b
--- /dev/null
+++ b/car/res/drawable/car_drawer_list_item_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_card_ripple_background">
+ <item android:id="@android:id/mask">
+ <color android:color="#ffffffff" />
+ </item>
+</ripple>
diff --git a/car/res/drawable/car_pagination_background.xml b/car/res/drawable/car_pagination_background.xml
new file mode 100644
index 0000000..6d3ad3e
--- /dev/null
+++ b/car/res/drawable/car_pagination_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_card_ripple_background" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_day.xml b/car/res/drawable/car_pagination_background_day.xml
new file mode 100644
index 0000000..a4370e9
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_day.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_card_ripple_background_dark" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_inverse.xml b/car/res/drawable/car_pagination_background_inverse.xml
new file mode 100644
index 0000000..3c07ecf
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_inverse.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_card_ripple_background_inverse" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_night.xml b/car/res/drawable/car_pagination_background_night.xml
new file mode 100644
index 0000000..c1b03c1
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_night.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/car_card_ripple_background_light" />
\ No newline at end of file
diff --git a/car/res/drawable/ic_down.xml b/car/res/drawable/ic_down.xml
new file mode 100644
index 0000000..c6bb32d
--- /dev/null
+++ b/car/res/drawable/ic_down.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="76dp"
+ android:height="76dp"
+ android:viewportWidth="76.0"
+ android:viewportHeight="76.0">
+ <path
+ android:pathData="M38,0.96C17.01,0.96 0,17.75 0,38.47C0,59.18 17.01,75.97 38,75.97C58.99,75.97 76,59.18 76,38.47C76,17.75 58.99,0.96 38,0.96M38,3.3C57.64,3.3 73.62,19.08 73.62,38.47C73.62,57.85 57.64,73.63 38,73.63C18.36,73.63 2.38,57.86 2.38,38.47C2.38,19.08 18.36,3.3 38,3.3"
+ android:strokeColor="#00000000"
+ android:fillColor="#212121"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M26.63,31.09l11.37,11.08l11.37,-11.08l3.5,3.42l-14.87,14.5l-14.87,-14.5z"
+ android:strokeColor="#00000000"
+ android:fillColor="#212121"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/car/res/drawable/ic_list_view_disable.xml b/car/res/drawable/ic_list_view_disable.xml
new file mode 100644
index 0000000..8649423
--- /dev/null
+++ b/car/res/drawable/ic_list_view_disable.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="176dp"
+ android:height="176dp"
+ android:viewportWidth="176.0"
+ android:viewportHeight="176.0">
+ <path
+ android:pathData="M88.99,55.55l15.71,15.71l46.13,0l0,-15.71z"
+ android:fillColor="#212121"/>
+ <path
+ android:pathData="M25.19,119.06h66.5v15.71h-66.5z"
+ android:fillColor="#212121"/>
+ <path
+ android:pathData="M114.58,103.35l-15.71,-15.71l-0.12,0l-16.38,-16.38l0.12,0l-15.71,-15.71l-0.12,0l-30.29,-30.29l-11.11,11.11l19.19,19.18l-19.28,0l0,15.71l34.98,0l16.39,16.38l-51.37,0l0,15.71l67.08,0l47.38,47.39l11.11,-11.11l-36.28,-36.28z"
+ android:fillColor="#212121"/>
+ <path
+ android:pathData="M136.79,103.35l14.04,0l0,-15.71l-29.74,0z"
+ android:fillColor="#212121"/>
+</vector>
diff --git a/car/res/drawable/ic_up.xml b/car/res/drawable/ic_up.xml
new file mode 100644
index 0000000..05f69b9
--- /dev/null
+++ b/car/res/drawable/ic_up.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="76dp"
+ android:height="76dp"
+ android:viewportWidth="76.0"
+ android:viewportHeight="76.0">
+ <path
+ android:pathData="M38,75.04C58.99,75.04 76,58.27 76,37.57C76,16.88 58.99,0.11 38,0.11C17.01,0.11 0,16.88 0,37.57C0,58.27 17.01,75.04 38,75.04M38,72.7C18.36,72.7 2.38,56.94 2.38,37.57C2.38,18.21 18.36,2.45 38,2.45C57.64,2.45 73.62,18.21 73.62,37.57C73.62,56.94 57.64,72.7 38,72.7"
+ android:strokeColor="#00000000"
+ android:fillColor="#212121"
+ android:strokeWidth="1"/>
+ <path
+ android:pathData="M49.37,44.9l-11.37,-11.08l-11.37,11.08l-3.5,-3.42l14.87,-14.5l14.87,14.5z"
+ android:strokeColor="#00000000"
+ android:fillColor="#212121"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/car/res/layout/car_drawer.xml b/car/res/layout/car_drawer.xml
new file mode 100644
index 0000000..812acb4
--- /dev/null
+++ b/car/res/layout/car_drawer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/drawer_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="@dimen/car_drawer_margin_end"
+ android:background="@color/car_card"
+ android:paddingTop="@dimen/car_app_bar_height" >
+
+ <android.support.car.widget.PagedListView
+ android:id="@+id/drawer_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:listEndMargin="@dimen/car_drawer_margin_end"
+ app:offsetScrollBar="true" />
+
+ <ProgressBar
+ android:id="@+id/drawer_progress"
+ android:layout_width="@dimen/car_drawer_progress_bar_size"
+ android:layout_height="@dimen/car_drawer_progress_bar_size"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:visibility="gone" />
+</FrameLayout>
diff --git a/car/res/layout/car_drawer_activity.xml b/car/res/layout/car_drawer_activity.xml
new file mode 100644
index 0000000..751ef0d
--- /dev/null
+++ b/car/res/layout/car_drawer_activity.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v4.widget.DrawerLayout
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- The main content view. Fragments will be added here. -->
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ layout="@layout/car_drawer" />
+ </android.support.v4.widget.DrawerLayout>
+
+ <include layout="@layout/car_toolbar" />
+</FrameLayout>
diff --git a/car/res/layout/car_drawer_list_item_empty.xml b/car/res/layout/car_drawer_list_item_empty.xml
new file mode 100644
index 0000000..c2e35ac
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_empty.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="16dp"
+ android:focusable="false"
+ android:orientation="vertical"
+ android:background="@drawable/car_drawer_list_item_background" >
+ <FrameLayout
+ android:id="@+id/icon_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="visible">
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="48dp"
+ android:layout_marginBottom="22dp" />
+ </FrameLayout>
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ style="@style/CarBody1" />
+</LinearLayout>
diff --git a/car/res/layout/car_drawer_list_item_normal.xml b/car/res/layout/car_drawer_list_item_normal.xml
new file mode 100644
index 0000000..9136aae
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_normal.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_double_line_list_item_height"
+ android:focusable="true"
+ android:orientation="horizontal"
+ android:background="@drawable/car_drawer_list_item_background" >
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+ android:layout_gravity="center_vertical"
+ android:scaleType="centerCrop" />
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_text_vertical_margin"
+ android:maxLines="1"
+ style="@style/CarBody1" />
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/CarBody2" />
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/end_icon"
+ android:layout_width="@dimen/car_drawer_list_item_end_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_end_icon_size"
+ android:scaleType="fitCenter"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_end_margin"
+ android:layout_gravity="center_vertical" />
+</LinearLayout>
diff --git a/car/res/layout/car_drawer_list_item_small.xml b/car/res/layout/car_drawer_list_item_small.xml
new file mode 100644
index 0000000..2818eef
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_small.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_single_line_list_item_height"
+ android:focusable="true"
+ android:orientation="horizontal"
+ android:background="@drawable/car_drawer_list_item_background" >
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/car_drawer_list_item_small_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_small_icon_size"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+ android:layout_gravity="center_vertical"
+ android:scaleType="centerCrop" />
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="@dimen/car_text_vertical_margin"
+ android:maxLines="1"
+ style="@style/CarBody1" />
+ <ImageView
+ android:id="@+id/end_icon"
+ android:layout_width="@dimen/car_drawer_list_item_end_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_end_icon_size"
+ android:scaleType="fitCenter"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_end_margin"
+ android:layout_gravity="center_vertical"/>
+</LinearLayout>
diff --git a/car/res/layout/car_paged_recycler_view.xml b/car/res/layout/car_paged_recycler_view.xml
new file mode 100644
index 0000000..47a82ff
--- /dev/null
+++ b/car/res/layout/car_paged_recycler_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.car.widget.CarRecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <!-- Putting this as the last child so that it can intercept any touch events on the
+ scroll buttons. -->
+ <android.support.car.widget.PagedScrollBarView
+ android:id="@+id/paged_scroll_view"
+ android:layout_width="@dimen/car_margin"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/car_scroll_bar_padding"
+ android:paddingTop="@dimen/car_scroll_bar_padding"
+ android:visibility="invisible" />
+</FrameLayout>
diff --git a/car/res/layout/car_paged_scrollbar_buttons.xml b/car/res/layout/car_paged_scrollbar_buttons.xml
new file mode 100644
index 0000000..7dd213a
--- /dev/null
+++ b/car/res/layout/car_paged_scrollbar_buttons.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/page_up"
+ android:layout_width="@dimen/car_scroll_bar_button_size"
+ android:layout_height="@dimen/car_scroll_bar_button_size"
+ android:background="@drawable/car_pagination_background"
+ android:focusable="false"
+ android:hapticFeedbackEnabled="false"
+ android:scaleType="center"
+ android:src="@drawable/ic_up" />
+
+ <FrameLayout
+ android:id="@+id/filler"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginBottom="@dimen/car_paged_list_view_scrollbar_thumb_margin"
+ android:layout_marginTop="@dimen/car_paged_list_view_scrollbar_thumb_margin" >
+
+ <ImageView
+ android:id="@+id/scrollbar_thumb"
+ android:layout_width="@dimen/car_scroll_bar_thumb_width"
+ android:layout_height="0dp"
+ android:layout_gravity="center_horizontal"
+ android:background="@color/car_scrollbar_thumb" />
+ </FrameLayout>
+
+ <ImageView
+ android:id="@+id/page_down"
+ android:layout_width="@dimen/car_scroll_bar_button_size"
+ android:layout_height="@dimen/car_scroll_bar_button_size"
+ android:background="@drawable/car_pagination_background"
+ android:focusable="false"
+ android:hapticFeedbackEnabled="false"
+ android:scaleType="center"
+ android:src="@drawable/ic_down" />
+</LinearLayout>
diff --git a/car/res/layout/car_toolbar.xml b/car/res/layout/car_toolbar.xml
new file mode 100644
index 0000000..88f05e3
--- /dev/null
+++ b/car/res/layout/car_toolbar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_app_bar_height">
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/car_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ style="@style/CarToolbarTheme" />
+</FrameLayout>
diff --git a/car/res/values-h1752dp/dimens.xml b/car/res/values-h1752dp/dimens.xml
new file mode 100644
index 0000000..feb8631
--- /dev/null
+++ b/car/res/values-h1752dp/dimens.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- Type Sizings -->
+ <dimen name="car_title_size">32sp</dimen>
+ <dimen name="car_title2_size">40sp</dimen>
+ <dimen name="car_headline1_size">56sp</dimen>
+ <dimen name="car_headline2_size">50sp</dimen>
+ <dimen name="car_body1_size">40sp</dimen>
+ <dimen name="car_body2_size">32sp</dimen>
+ <dimen name="car_action1_size">32sp</dimen>
+
+ <!-- Car Component Dimensions -->
+ <!-- Application Bar Height -->
+ <dimen name="car_app_bar_height">112dp</dimen>
+
+ <dimen name="car_touch_target">96dp</dimen>
+
+ <!-- Icon dimensions -->
+ <dimen name="car_primary_icon_size">56dp</dimen>
+ <dimen name="car_secondary_icon_size">36dp</dimen>
+
+ <!-- Line heights -->
+ <dimen name="car_single_line_list_item_height">128dp</dimen>
+ <dimen name="car_double_line_list_item_height">128dp</dimen>
+</resources>
diff --git a/car/res/values-h684dp/dimens.xml b/car/res/values-h684dp/dimens.xml
new file mode 100644
index 0000000..a072681
--- /dev/null
+++ b/car/res/values-h684dp/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- Car Component Dimensions -->
+ <dimen name="car_app_bar_height">96dp</dimen>
+
+ <!-- List and Drawer Dimensions -->
+ <dimen name="car_drawer_list_item_icon_size">108dp</dimen>
+ <dimen name="car_drawer_list_item_small_icon_size">56dp</dimen>
+ <dimen name="car_drawer_list_item_end_icon_size">56dp</dimen>
+
+ <!-- Line heights -->
+ <dimen name="car_single_line_list_item_height">116dp</dimen>
+ <dimen name="car_double_line_list_item_height">116dp</dimen>
+</resources>
diff --git a/car/res/values-night/colors.xml b/car/res/values-night/colors.xml
new file mode 100644
index 0000000..2ca5b02
--- /dev/null
+++ b/car/res/values-night/colors.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <color name="car_title">@color/car_title_light</color>
+ <color name="car_body1">@color/car_body1_light</color>
+ <color name="car_body2">@color/car_body2_light</color>
+
+ <color name="car_tint">@color/car_tint_light</color>
+ <color name="car_tint_inverse">@color/car_tint_dark</color>
+
+ <color name="car_card">@color/car_card_dark</color>
+ <color name="car_card_ripple_background">@color/car_card_ripple_background_light</color>
+ <color name="car_card_ripple_background_inverse">@color/car_card_ripple_background_dark</color>
+
+ <color name="car_list_divider">@color/car_list_divider_dark</color>
+ <color name="car_scrollbar_thumb">@color/car_scrollbar_thumb_light</color>
+ <color name="car_scrollbar_thumb_inverse">@color/car_scrollbar_thumb_dark</color>
+</resources>
diff --git a/car/res/values-w1024dp/dimens.xml b/car/res/values-w1024dp/dimens.xml
new file mode 100644
index 0000000..b1ae5ba
--- /dev/null
+++ b/car/res/values-w1024dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_screen_margin_size">112dp</dimen>
+</resources>
diff --git a/car/res/values-w1280dp/dimens.xml b/car/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..9837355
--- /dev/null
+++ b/car/res/values-w1280dp/dimens.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_screen_margin_size">148dp</dimen>
+ <dimen name="car_scroll_bar_button_size">76dp</dimen>
+
+ <dimen name="car_keyline_1">32dp</dimen>
+ <dimen name="car_keyline_2">108dp</dimen>
+ <dimen name="car_keyline_3">128dp</dimen>
+ <dimen name="car_keyline_4">182dp</dimen>
+ <dimen name="car_keyline_1_neg">32dp</dimen>
+ <dimen name="car_keyline_2_neg">108dp</dimen>
+ <dimen name="car_keyline_3_neg">128dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="car_margin">148dp</dimen>
+</resources>
diff --git a/car/res/values-w1920dp/dimens.xml b/car/res/values-w1920dp/dimens.xml
new file mode 100644
index 0000000..52962a1
--- /dev/null
+++ b/car/res/values-w1920dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_keyline_3">152dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="car_margin">192dp</dimen>
+</resources>
diff --git a/car/res/values-w480dp/dimens.xml b/car/res/values-w480dp/dimens.xml
new file mode 100644
index 0000000..4077e0d
--- /dev/null
+++ b/car/res/values-w480dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_screen_margin_size">24dp</dimen>
+</resources>
diff --git a/car/res/values-w600dp/integers.xml b/car/res/values-w600dp/integers.xml
new file mode 100644
index 0000000..5dcd8df
--- /dev/null
+++ b/car/res/values-w600dp/integers.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <integer name="car_screen_num_of_columns">8</integer>
+ <integer name="column_card_default_column_span">6</integer>
+</resources>
diff --git a/car/res/values-w690dp/dimens.xml b/car/res/values-w690dp/dimens.xml
new file mode 100644
index 0000000..f797955
--- /dev/null
+++ b/car/res/values-w690dp/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_keyline_1">24dp</dimen>
+ <dimen name="car_keyline_2">96dp</dimen>
+ <dimen name="car_keyline_3">112dp</dimen>
+ <dimen name="car_keyline_4">148dp</dimen>
+ <dimen name="car_keyline_1_neg">-24dp</dimen>
+ <dimen name="car_keyline_2_neg">-96dp</dimen>
+ <dimen name="car_keyline_3_neg">-112dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="car_margin">112dp</dimen>
+</resources>
diff --git a/car/res/values-w720dp/dimens.xml b/car/res/values-w720dp/dimens.xml
new file mode 100644
index 0000000..b1ae5ba
--- /dev/null
+++ b/car/res/values-w720dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_screen_margin_size">112dp</dimen>
+</resources>
diff --git a/car/res/values-w840dp/dimens.xml b/car/res/values-w840dp/dimens.xml
new file mode 100644
index 0000000..8b4d992
--- /dev/null
+++ b/car/res/values-w840dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_keyline_1">32dp</dimen>
+ <dimen name="car_keyline_2">108dp</dimen>
+ <dimen name="car_keyline_3">128dp</dimen>
+ <dimen name="car_screen_gutter_size">24dp</dimen>
+</resources>
diff --git a/car/res/values-w840dp/integers.xml b/car/res/values-w840dp/integers.xml
new file mode 100644
index 0000000..38c0440
--- /dev/null
+++ b/car/res/values-w840dp/integers.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <integer name="car_screen_num_of_columns">12</integer>
+ <integer name="column_card_default_column_span">8</integer>
+</resources>
diff --git a/car/res/values-w930dp/dimens.xml b/car/res/values-w930dp/dimens.xml
new file mode 100644
index 0000000..481480e
--- /dev/null
+++ b/car/res/values-w930dp/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <dimen name="car_keyline_1">32dp</dimen>
+ <dimen name="car_keyline_2">108dp</dimen>
+ <dimen name="car_keyline_3">128dp</dimen>
+ <dimen name="car_keyline_4">168dp</dimen>
+ <dimen name="car_keyline_1_neg">-32dp</dimen>
+ <dimen name="car_keyline_2_neg">-108dp</dimen>
+ <dimen name="car_keyline_3_neg">-128dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="car_margin">112dp</dimen>
+</resources>
diff --git a/car/res/values/attrs.xml b/car/res/values/attrs.xml
new file mode 100644
index 0000000..0ba8f55
--- /dev/null
+++ b/car/res/values/attrs.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- The configurable attributes for a ColumnCardView. -->
+ <declare-styleable name="ColumnCardView">
+ <!-- The number of columns that this ColumnCardView should span across. This value will
+ determine the width of the card. -->
+ <attr name="columnSpan" format="integer" />
+ </declare-styleable>
+
+ <!-- The configurable attributes in PagedListView. -->
+ <declare-styleable name="PagedListView">
+ <!-- Fade duration in ms -->
+ <attr name="fadeLastItem" format="boolean" />
+ <!-- Set to true/false to offset rows as they slide off screen. Defaults to true -->
+ <attr name="offsetRows" format="boolean" />
+ <!-- Whether or not to offset the list view by the width of scroll bar. Setting this to
+ true will ensure that any views within the list will not overlap the scroll bar. -->
+ <attr name="offsetScrollBar" format="boolean" />
+ <!-- Whether to display the scrollbar or not. Defaults to true. -->
+ <attr name="scrollBarEnabled" format="boolean" />
+ <!-- Whether or not to show a diving line between each item of the list. -->
+ <attr name="showPagedListViewDivider" format="boolean" />
+ <!-- An optional id that specifies a child View whose starting edge will be used to
+ determine the start position of the dividing line. -->
+ <attr name="alignDividerStartTo" format="reference" />
+ <!-- An optional id that specifies a child View whose ending edge will be used to
+ determine the end position of the dividing line. -->
+ <attr name="alignDividerEndTo" format="reference" />
+ <!-- A starting margin before the drawing of the dividing line. This margin will be an
+ offset from the view specified by "alignDividerStartTo" if given. -->
+ <attr name="dividerStartMargin" format="dimension" />
+ <!-- The width of the margin on the right side of the list -->
+ <attr name="listEndMargin" format="dimension" />
+ <!-- An optional spacing between items in the list -->
+ <attr name="itemSpacing" format="dimension" />
+ <!-- The icon to be used for the up button of the scroll bar. -->
+ <attr name="upButtonIcon" format="reference" />
+ <!-- The icon to be used for the down button of the scroll bar. -->
+ <attr name="downButtonIcon" format="reference" />
+ </declare-styleable>
+
+ <!-- The attributes for customizing the appearance of the hamburger and back arrow in the
+ drawer. -->
+ <declare-styleable name="DrawerArrowDrawable">
+ <!-- The color of the arrow. -->
+ <attr name="carArrowColor" format="color"/>
+ <!-- Whether the arrow will animate when switches directions. -->
+ <attr name="carArrowAnimate" format="boolean"/>
+ <!-- The size of the arrow's bounding box. -->
+ <attr name="carArrowSize" format="dimension"/>
+ <!-- The length of the top and bottom bars that merge to form the point of the arrow. -->
+ <attr name="carArrowHeadLength" format="dimension"/>
+ <!-- The length of arrow shaft. -->
+ <attr name="carArrowShaftLength" format="dimension"/>
+ <!-- The thickness of each of the bars that form the arrow. -->
+ <attr name="carArrowThickness" format="dimension"/>
+ <!-- The spacing between the menu bars (i.e. the "hamburger" icon). -->
+ <attr name="carMenuBarSpacing" format="dimension"/>
+ <!-- The size of the menu bars (i.e. the "hamburger" icon). -->
+ <attr name="carMenuBarThickness" format="dimension"/>
+ </declare-styleable>
+</resources>
diff --git a/car/res/values/colors.xml b/car/res/values/colors.xml
new file mode 100644
index 0000000..00c6cf9
--- /dev/null
+++ b/car/res/values/colors.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- These colors are from
+ http://www.google.com/design/spec/style/color.html#color-ui-color-palette -->
+ <color name="car_grey_50">#fffafafa</color>
+ <color name="car_grey_100">#fff5f5f5</color>
+ <color name="car_grey_200">#ffeeeeee</color>
+ <color name="car_grey_300">#ffe0e0e0</color>
+ <color name="car_grey_400">#ffbdbdbd</color>
+ <color name="car_grey_500">#ff9e9e9e</color>
+ <color name="car_grey_600">#ff757575</color>
+ <color name="car_grey_650">#ff6B6B6B</color>
+ <color name="car_grey_700">#ff616161</color>
+ <color name="car_grey_800">#ff424242</color>
+ <color name="car_grey_900">#ff212121</color>
+ <color name="car_grey_1000">#cc000000</color>
+ <color name="car_white_1000">#1effffff</color>
+ <color name="car_blue_grey_800">#ff37474F</color>
+ <color name="car_blue_grey_900">#ff263238</color>
+ <color name="car_dark_blue_grey_600">#ff1d272d</color>
+ <color name="car_dark_blue_grey_700">#ff172026</color>
+ <color name="car_dark_blue_grey_800">#ff11181d</color>
+ <color name="car_dark_blue_grey_900">#ff0c1013</color>
+ <color name="car_dark_blue_grey_1000">#ff090c0f</color>
+ <color name="car_light_blue_300">#ff4fc3f7</color>
+ <color name="car_light_blue_500">#ff03A9F4</color>
+ <color name="car_light_blue_600">#ff039be5</color>
+ <color name="car_light_blue_700">#ff0288d1</color>
+ <color name="car_light_blue_800">#ff0277bd</color>
+ <color name="car_light_blue_900">#ff01579b</color>
+ <color name="car_blue_300">#ff91a7ff</color>
+ <color name="car_blue_500">#ff5677fc</color>
+ <color name="car_green_500">#ff0f9d58</color>
+ <color name="car_green_700">#ff0b8043</color>
+ <color name="car_yellow_500">#fff4b400</color>
+ <color name="car_yellow_800">#ffee8100</color>
+ <color name="car_red_400">#ffe06055</color>
+ <color name="car_red_500">#ffdb4437</color>
+ <color name="car_red_500a">#ffd50000</color>
+ <color name="car_red_700">#ffc53929</color>
+ <color name="car_teal_200">#ff80cbc4</color>
+ <color name="car_teal_700">#ff00796b</color>
+ <color name="car_indigo_800">#ff283593</color>
+
+ <!-- Various colors for text sizes. "Light" and "dark" here refer to the lighter or darker
+ shades. -->
+ <color name="car_title_light">@color/car_grey_100</color>
+ <color name="car_title_dark">@color/car_grey_900</color>
+ <color name="car_title">@color/car_title_dark</color>
+
+ <color name="car_headline1_light">@color/car_grey_100</color>
+ <color name="car_headline1_dark">@color/car_grey_800</color>
+ <color name="car_headline1">@color/car_headline1_dark</color>
+
+ <color name="car_headline2_light">@color/car_grey_100</color>
+ <color name="car_headline2_dark">@color/car_grey_900</color>
+ <color name="car_headline2">@color/car_headline2_dark</color>
+
+ <color name="car_headline3_light">@android:color/white</color>
+ <color name="car_headline3_dark">@color/car_grey_900</color>
+ <color name="car_headline3">@color/car_headline3_dark</color>
+
+ <color name="car_headline4_light">@android:color/white</color>
+ <color name="car_headline4_dark">@android:color/black</color>
+ <color name="car_headline4">@color/car_headline4_dark</color>
+
+ <color name="car_body1_light">@color/car_grey_100</color>
+ <color name="car_body1_dark">@color/car_grey_900</color>
+ <color name="car_body1">@color/car_body1_dark</color>
+
+ <color name="car_body2_light">@color/car_grey_300</color>
+ <color name="car_body2_dark">@color/car_grey_650</color>
+ <color name="car_body2">@color/car_body2_dark</color>
+
+ <color name="car_body3_light">@android:color/white</color>
+ <color name="car_body3_dark">@android:color/black</color>
+ <color name="car_body3">@color/car_body3_dark</color>
+
+ <color name="car_body4_light">@android:color/white</color>
+ <color name="car_body4_dark">@android:color/black</color>
+ <color name="car_body4">@color/car_body4_dark</color>
+
+ <color name="car_action1_light">@color/car_grey_900</color>
+ <color name="car_action1_dark">@color/car_grey_50</color>
+ <color name="car_action1">@color/car_action1_dark</color>
+
+ <!-- The tinting colors to create a light- and dark-colored icon respectively. -->
+ <color name="car_tint_light">@color/car_grey_50</color>
+ <color name="car_tint_dark">@color/car_grey_900</color>
+
+ <!-- The tinting color for an icon. This icon is assumed to be on a light background. -->
+ <color name="car_tint">@color/car_tint_dark</color>
+
+ <!-- An inverted tinting from car_tint. -->
+ <color name="car_tint_inverse">@color/car_tint_light</color>
+
+ <!-- The color of the divider. The color here is a lighter shade. -->
+ <color name="car_list_divider_light">#1fffffff</color>
+
+ <!-- The color of the divider. The color here is a darker shade. -->
+ <color name="car_list_divider_dark">#1f000000</color>
+
+ <!-- The color of the dividers in the list. This color is assumed to be on a light colored
+ view. -->
+ <color name="car_list_divider">@color/car_list_divider_dark</color>
+
+ <!-- A light and dark colored card. -->
+ <color name="car_card_light">@color/car_grey_50</color>
+ <color name="car_card_dark">@color/car_dark_blue_grey_700</color>
+
+ <!-- The default color of a card in car UI. -->
+ <color name="car_card">@color/car_card_light</color>
+
+ <!-- The ripple colors. The "dark" and "light" designation here refers to the color of the
+ ripple itself. -->
+ <color name="car_card_ripple_background_dark">#8F000000</color>
+ <color name="car_card_ripple_background_light">#27ffffff</color>
+
+ <!-- The ripple color for a light colored card. -->
+ <color name="car_card_ripple_background">@color/car_card_ripple_background_dark</color>
+
+ <!-- The ripple color for a dark-colored card. This color is the opposite of
+ car_card_ripple_background. -->
+ <color name="car_card_ripple_background_inverse">@color/car_card_ripple_background_light</color>
+
+ <!-- The top margin before the start of content in an application. -->
+ <dimen name="app_header_height">96dp</dimen>
+
+ <!-- The lighter and darker color for the scrollbar thumb. -->
+ <color name="car_scrollbar_thumb_light">#99ffffff</color>
+ <color name="car_scrollbar_thumb_dark">#7f0b0f12</color>
+
+ <!-- The color of the scroll bar indicator in the PagedListView. This color is assumed to be on
+ a light-colored background. -->
+ <color name="car_scrollbar_thumb">@color/car_scrollbar_thumb_dark</color>
+
+ <!-- The inverted color of the scroll bar indicator. This color is always the opposite of
+ car_scrollbar_thumb. -->
+ <color name="car_scrollbar_thumb_inverse">@color/car_scrollbar_thumb_light</color>
+
+ <!-- Misc colors -->
+ <color name="car_highlight_light">@color/car_teal_700</color>
+ <color name="car_highlight_dark">@color/car_teal_200</color>
+ <color name="car_highlight">@color/car_highlight_light</color>
+</resources>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
new file mode 100644
index 0000000..1ffabd5
--- /dev/null
+++ b/car/res/values/dimens.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- Keylines for content. -->
+ <dimen name="car_keyline_1">48dp</dimen>
+ <dimen name="car_keyline_2">108dp</dimen>
+ <dimen name="car_keyline_3">152dp</dimen>
+ <dimen name="car_keyline_4">182dp</dimen>
+ <dimen name="car_keyline_1_neg">-48dp</dimen>
+ <dimen name="car_keyline_2_neg">-108dp</dimen>
+ <dimen name="car_keyline_3_neg">-152dp</dimen>
+
+ <!-- Type Sizings -->
+ <dimen name="car_title2_size">32sp</dimen>
+ <dimen name="car_headline1_size">45sp</dimen>
+ <dimen name="car_headline2_size">36sp</dimen>
+ <dimen name="car_headline3_size">24sp</dimen>
+ <dimen name="car_headline4_size">20sp</dimen>
+ <dimen name="car_body1_size">32sp</dimen>
+ <dimen name="car_body2_size">26sp</dimen>
+ <dimen name="car_body3_size">16sp</dimen>
+ <dimen name="car_body4_size">14sp</dimen>
+ <dimen name="car_body5_size">18sp</dimen>
+ <dimen name="car_action1_size">26sp</dimen>
+
+ <!-- Paddings -->
+ <dimen name="car_padding_1">4dp</dimen>
+ <dimen name="car_padding_2">10dp</dimen>
+ <dimen name="car_padding_3">16dp</dimen>
+ <dimen name="car_padding_4">28dp</dimen>
+ <dimen name="car_padding_5">32dp</dimen>
+
+ <!-- Radius -->
+ <dimen name="car_radius_1">4dp</dimen>
+ <dimen name="car_radius_2">8dp</dimen>
+ <dimen name="car_radius_3">16dp</dimen>
+ <dimen name="car_radius_5">100dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="car_margin">20dp</dimen>
+
+ <!-- Car Component Dimensions -->
+ <!-- Application Bar Height -->
+ <dimen name="car_app_bar_height">80dp</dimen>
+
+ <!-- The height of the bar that contains an applications action buttons. -->
+ <dimen name="car_action_bar_height">128dp</dimen>
+
+ <!-- Minimum touch target size. -->
+ <dimen name="car_touch_target">76dp</dimen>
+
+ <!-- Button Dimensions -->
+ <dimen name="car_button_height">64dp</dimen>
+ <dimen name="car_button_min_width">158dp</dimen>
+ <dimen name="car_button_horizontal_padding">@dimen/car_padding_4</dimen>
+ <dimen name="car_button_radius">@dimen/car_radius_1</dimen>
+
+ <!-- Icon dimensions -->
+ <dimen name="car_primary_icon_size">44dp</dimen>
+ <dimen name="car_secondary_icon_size">24dp</dimen>
+
+ <!-- Line heights -->
+ <dimen name="car_single_line_list_item_height">76dp</dimen>
+ <dimen name="car_double_line_list_item_height">96dp</dimen>
+
+ <!-- List and Drawer Dimensions -->
+ <!-- The margin on both sides of the screen before the contents of the PagedListView. -->
+ <dimen name="car_card_margin">96dp</dimen>
+
+ <!-- The height of the dividers in the list. -->
+ <dimen name="car_divider_height">1dp</dimen>
+
+ <!-- Sample row height used for scroll bar calculations in the off chance that a view hasn't
+ been measured. It's highly unlikely that this value will actually be used for more than
+ a frame max. The sample row is a 96dp card + 16dp margin on either side. -->
+ <dimen name="car_sample_row_height">128dp</dimen>
+
+ <!-- The amount of space the LayoutManager will make sure the last item on the screen is
+ peeking before scrolling down -->
+ <dimen name="car_last_card_peek_amount">16dp</dimen>
+
+ <!-- The spacing between each column that fits on the screen. The number of columns is
+ determined by integer/car_screen_num_of_columns. -->
+ <dimen name="car_screen_gutter_size">16dp</dimen>
+
+ <!-- The margin on both sizes of the scroll bar thumb. -->
+ <dimen name="car_paged_list_view_scrollbar_thumb_margin">8dp</dimen>
+
+ <!-- The size of the scroll bar up and down arrows. -->
+ <dimen name="car_scroll_bar_button_size">44dp</dimen>
+
+ <!-- The padding around the scroll bar. -->
+ <dimen name="car_scroll_bar_padding">16dp</dimen>
+
+ <!-- The width of the scroll bar thumb. -->
+ <dimen name="car_scroll_bar_thumb_width">6dp</dimen>
+
+ <!-- The minimum the scrollbar thumb can shrink to -->
+ <dimen name="min_thumb_height">48dp</dimen>
+
+ <!-- The maximum the scrollbar thumb can grow to -->
+ <dimen name="max_thumb_height">128dp</dimen>
+
+ <!-- Size of progress-bar in Drawer -->
+ <dimen name="car_drawer_progress_bar_size">48dp</dimen>
+
+ <!-- The ending margin of the drawer. Is is the amount that the navigation drawer does not
+ cover the screen. -->
+ <dimen name="car_drawer_margin_end">96dp</dimen>
+
+ <!-- Dimensions of the back arrow in the drawer. -->
+ <dimen name="car_arrow_size">96dp</dimen>
+ <dimen name="car_arrow_thickness">3dp</dimen>
+ <dimen name="car_arrow_shaft_length">34dp</dimen>
+ <dimen name="car_arrow_head_length">18dp</dimen>
+ <dimen name="car_menu_bar_spacing">6dp</dimen>
+ <dimen name="car_menu_bar_length">40dp</dimen>
+
+ <!-- The size of the starting icon. -->
+ <dimen name="car_drawer_list_item_icon_size">64dp</dimen>
+
+ <!-- The margin after the starting icon. -->
+ <dimen name="car_drawer_list_item_icon_end_margin">32dp</dimen>
+
+ <!-- The ending margin on a list view. -->
+ <dimen name="car_drawer_list_item_end_margin">32dp</dimen>
+
+ <!-- The size of the starting icon in a small list item.-->
+ <dimen name="car_drawer_list_item_small_icon_size">56dp</dimen>
+
+ <!-- The size of the ending icon in a list item. -->
+ <dimen name="car_drawer_list_item_end_icon_size">56dp</dimen>
+
+ <!-- The margin between text is lies on top of each other. -->
+ <dimen name="car_text_vertical_margin">2dp</dimen>
+</resources>
diff --git a/car/res/values/integers.xml b/car/res/values/integers.xml
new file mode 100644
index 0000000..575d646
--- /dev/null
+++ b/car/res/values/integers.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- The number of columns that appear on-screen. -->
+ <integer name="car_screen_num_of_columns">4</integer>
+
+ <!-- The default number of columns that a ColumnCardView will span if columnSpan is not
+ specified.-->
+ <integer name="column_card_default_column_span">4</integer>
+</resources>
diff --git a/car/res/values/strings.xml b/car/res/values/strings.xml
new file mode 100644
index 0000000..65f08b6
--- /dev/null
+++ b/car/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- NOTE: Although these strings won't really be used for accessibility
+ in an auto context, integration tests will use them to open/close
+ drawer. See:
+ google_testing/integration/libraries/app-helpers/first-party/auto/
+ -->
+ <string name="car_drawer_open" translatable="false">Open drawer</string>
+ <string name="car_drawer_close" translatable="false">Close drawer</string>
+</resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
new file mode 100644
index 0000000..61e089b
--- /dev/null
+++ b/car/res/values/styles.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- The styling for title text. The color of this text changes based on day/night mode. -->
+ <style name="CarTitle" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_title_size</item>
+ <item name="android:textColor">@color/car_title</item>
+ </style>
+
+ <!-- Title text that is permanently a dark color. -->
+ <style name="CarTitle.Dark" >
+ <item name="android:textColor">@color/car_title_dark</item>
+ </style>
+
+ <!-- Title text that is permanently a light color. -->
+ <style name="CarTitle.Light" >
+ <item name="android:textColor">@color/car_title_light</item>
+ </style>
+
+ <!-- The styling for the main headline text. The color of this text changes based on the
+ day/night mode. -->
+ <style name="CarHeadline1" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_headline1_size</item>
+ <item name="android:textColor">@color/car_headline1</item>
+ </style>
+
+ <!-- The styling for a sub-headline text. The color of this text changes based on the
+ day/night mode. -->
+ <style name="CarHeadline2" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_headline2_size</item>
+ <item name="android:textColor">@color/car_headline2</item>
+ </style>
+
+ <!-- The styling for a smaller alternate headline text. The color of this text changes based on
+ the day/night mode. -->
+ <style name="CarHeadline3" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_headline3_size</item>
+ <item name="android:textColor">@color/car_headline3</item>
+ </style>
+
+ <!-- The styling for the smallest headline text. The color of this text changes based on the
+ day/night mode. -->
+ <style name="CarHeadline4" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_headline4_size</item>
+ <item name="android:textColor">@color/car_headline4</item>
+ </style>
+
+ <!-- The styling for body text. The color of this text changes based on the day/night mode. -->
+ <style name="CarBody1" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_body1_size</item>
+ <item name="android:textColor">@color/car_body1</item>
+ </style>
+
+ <!-- An alternate styling for body text that is both a different color and size than
+ CarBody1. -->
+ <style name="CarBody2" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_body2_size</item>
+ <item name="android:textColor">@color/car_body2</item>
+ </style>
+
+ <!-- A smaller styling for body text. The color of this text changes based on the day/night
+ mode. -->
+ <style name="CarBody3" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_body3_size</item>
+ <item name="android:textColor">@color/car_body3</item>
+ </style>
+
+ <!-- The smallest styling for body text. The color of this text changes based on the day/night
+ mode. -->
+ <style name="CarBody4" >
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_body4_size</item>
+ <item name="android:textColor">@color/car_body4</item>
+ </style>
+
+ <!-- The style for the menu bar (i.e. hamburger) and back arrow in the navigation drawer. -->
+ <style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
+ <item name="color">@color/car_title_light</item>
+ <item name="spinBars">true</item>
+ <item name="barLength">@dimen/car_menu_bar_length</item>
+ <item name="thickness">@dimen/car_arrow_thickness</item>
+ <item name="gapBetweenBars">@dimen/car_menu_bar_spacing</item>
+ <item name="arrowShaftLength">@dimen/car_arrow_shaft_length</item>
+ <item name="arrowHeadLength">@dimen/car_arrow_head_length</item>
+ <item name="drawableSize">@dimen/car_arrow_size</item>
+ </style>
+
+ <!-- The styles for the regular and borderless buttons -->
+ <style name="CarButton" parent="android:Widget.Material.Button">
+ <item name="android:layout_height">@dimen/car_button_height</item>
+ <item name="android:minWidth">@dimen/car_button_min_width</item>
+ <item name="android:paddingStart">@dimen/car_button_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/car_button_horizontal_padding</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_action1_size</item>
+ <item name="android:textColor">@drawable/car_button_text_color</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:background">@drawable/car_button_background</item>
+ </style>
+
+ <style name="CarBorderlessButton" parent="android:Widget.Material.Button.Borderless">
+ <item name="android:layout_height">@dimen/car_button_height</item>
+ <item name="android:minWidth">@dimen/car_button_min_width</item>
+ <item name="android:paddingStart">@dimen/car_button_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/car_button_horizontal_padding</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_action1_size</item>
+ <item name="android:textColor">@drawable/car_button_text_color</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+</resources>
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
new file mode 100644
index 0000000..4244a22
--- /dev/null
+++ b/car/res/values/themes.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <!-- A Theme that activities should use to have correct arrow styling. -->
+ <style name="CarDrawerActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
+ <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
+ </style>
+
+ <!-- The styling for the action bar. -->
+ <style name="CarToolbarTheme">
+ <item name="titleTextAppearance">@style/CarTitle.Light</item>
+ <item name="contentInsetStart">@dimen/car_keyline_1</item>
+ <item name="contentInsetEnd">@dimen/car_keyline_1</item>
+ </style>
+</resources>
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java b/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java
new file mode 100644
index 0000000..f46c652
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.drawer;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Common base Activity for car apps that need to present a Drawer.
+ *
+ * <p>This Activity manages the overall layout. To use it, sub-classes need to:
+ *
+ * <ul>
+ * <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}.
+ * <li>Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}.
+ * They can also add fragments to the main-content container by obtaining its id using
+ * {@link #getContentContainerId()}
+ * </ul>
+ *
+ * <p>This class will take care of drawer toggling and display.
+ *
+ * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the
+ * CarDrawerAdapter for the next level to
+ * {@link CarDrawerController#pushAdapter(CarDrawerAdapter)}.
+ *
+ * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a
+ * derivative.
+ */
+public abstract class CarDrawerActivity extends AppCompatActivity {
+ private CarDrawerController mDrawerController;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.car_drawer_activity);
+
+ DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
+ ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
+ this /* activity */,
+ drawerLayout, /* DrawerLayout object */
+ R.string.car_drawer_open,
+ R.string.car_drawer_close);
+
+ Toolbar toolbar = findViewById(R.id.car_toolbar);
+ setSupportActionBar(toolbar);
+
+ mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle);
+ mDrawerController.setRootAdapter(getRootAdapter());
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+
+ /**
+ * Returns the {@link CarDrawerController} that is responsible for handling events relating
+ * to the drawer in this Activity.
+ *
+ * @return The {@link CarDrawerController} linked to this Activity. This value will be
+ * {@code null} if this method is called before {@code onCreate()} has been called.
+ */
+ @Nullable
+ protected CarDrawerController getDrawerController() {
+ return mDrawerController;
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mDrawerController.syncState();
+ }
+
+ /**
+ * @return Adapter for root content of the Drawer.
+ */
+ protected abstract CarDrawerAdapter getRootAdapter();
+
+ /**
+ * Set main content to display in this Activity. It will be added to R.id.content_frame in
+ * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}.
+ *
+ * @param view View to display as main content.
+ */
+ public void setMainContent(View view) {
+ ViewGroup parent = findViewById(getContentContainerId());
+ parent.addView(view);
+ }
+
+ /**
+ * Set main content to display in this Activity. It will be added to R.id.content_frame in
+ * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}.
+ *
+ * @param resourceId Layout to display as main content.
+ */
+ public void setMainContent(@LayoutRes int resourceId) {
+ ViewGroup parent = findViewById(getContentContainerId());
+ LayoutInflater inflater = getLayoutInflater();
+ inflater.inflate(resourceId, parent, true);
+ }
+
+ /**
+ * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own
+ * content/fragments inside here.
+ *
+ * @return Id of FrameLayout where main content of the subclass Activity can be added.
+ */
+ protected int getContentContainerId() {
+ return R.id.content_frame;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mDrawerController.closeDrawer();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDrawerController.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+}
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java b/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java
new file mode 100644
index 0000000..b0fd965
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.drawer;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.car.widget.PagedListView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Base adapter for displaying items in the car navigation drawer, which uses a
+ * {@link PagedListView}.
+ *
+ * <p>Subclasses must set the title that will be displayed when displaying the contents of the
+ * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The
+ * title of the root adapter will also be the main title showed in the toolbar when the drawer is
+ * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information.
+ *
+ * <p>This class also takes care of implementing the PageListView.ItemCamp contract and subclasses
+ * should implement {@link #getActualItemCount()}.
+ */
+public abstract class CarDrawerAdapter extends RecyclerView.Adapter<DrawerItemViewHolder>
+ implements PagedListView.ItemCap, DrawerItemClickListener {
+ private final boolean mShowDisabledListOnEmpty;
+ private final Drawable mEmptyListDrawable;
+ private int mMaxItems = PagedListView.ItemCap.UNLIMITED;
+ private CharSequence mTitle;
+ private TitleChangeListener mTitleChangeListener;
+
+ /**
+ * Interface for a class that will be notified a new title has been set on this adapter.
+ */
+ interface TitleChangeListener {
+ /**
+ * Called when {@link #setTitle(CharSequence)} has been called and the title has been
+ * changed.
+ */
+ void onTitleChanged(CharSequence newTitle);
+ }
+
+ protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) {
+ mShowDisabledListOnEmpty = showDisabledListOnEmpty;
+
+ mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable);
+ mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint),
+ PorterDuff.Mode.SRC_IN);
+ }
+
+ /** Returns the title set via {@link #setTitle(CharSequence)}. */
+ CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /** Updates the title to display in the toolbar for this Adapter. */
+ public final void setTitle(@NonNull CharSequence title) {
+ if (title == null) {
+ throw new IllegalArgumentException("setTitle() cannot be passed a null title!");
+ }
+
+ mTitle = title;
+
+ if (mTitleChangeListener != null) {
+ mTitleChangeListener.onTitleChanged(mTitle);
+ }
+ }
+
+ /** Sets a listener to be notified whenever the title of this adapter has been changed. */
+ void setTitleChangeListener(@Nullable TitleChangeListener listener) {
+ mTitleChangeListener = listener;
+ }
+
+ @Override
+ public final void setMaxItems(int maxItems) {
+ mMaxItems = maxItems;
+ }
+
+ @Override
+ public final int getItemCount() {
+ if (shouldShowDisabledListItem()) {
+ return 1;
+ }
+ return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount();
+ }
+
+ /**
+ * Returns the absolute number of items that can be displayed in the list.
+ *
+ * <p>A class should implement this method to supply the number of items to be displayed.
+ * Returning 0 from this method will cause an empty list icon to be displayed in the drawer.
+ *
+ * <p>A class should override this method rather than {@link #getItemCount()} because that
+ * method is handling the logic of when to display the empty list icon. It will return 1 when
+ * {@link #getActualItemCount()} returns 0.
+ *
+ * @return The number of items to be displayed in the list.
+ */
+ protected abstract int getActualItemCount();
+
+ @Override
+ public final int getItemViewType(int position) {
+ if (shouldShowDisabledListItem()) {
+ return R.layout.car_drawer_list_item_empty;
+ }
+
+ return usesSmallLayout(position)
+ ? R.layout.car_drawer_list_item_small
+ : R.layout.car_drawer_list_item_normal;
+ }
+
+ /**
+ * Used to indicate the layout used for the Drawer item at given position. Subclasses can
+ * override this to use normal layout which includes text element below title.
+ *
+ * <p>A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}.
+ * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used.
+ *
+ * @param position Adapter position of item.
+ * @return Whether the item at this position will use a small layout (default) or normal layout.
+ */
+ protected boolean usesSmallLayout(int position) {
+ return true;
+ }
+
+ @Override
+ public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
+ return new DrawerItemViewHolder(view);
+ }
+
+ @Override
+ public final void onBindViewHolder(DrawerItemViewHolder holder, int position) {
+ if (shouldShowDisabledListItem()) {
+ holder.getTitle().setText(null);
+ holder.getIcon().setImageDrawable(mEmptyListDrawable);
+ holder.setItemClickListener(null);
+ } else {
+ holder.setItemClickListener(this);
+ populateViewHolder(holder, position);
+ }
+ }
+
+ /**
+ * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it
+ * has been configured to show and there are no items to be displayed.
+ */
+ private boolean shouldShowDisabledListItem() {
+ return mShowDisabledListOnEmpty && getActualItemCount() == 0;
+ }
+
+ /**
+ * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some
+ * element is not used, it should be nulled out since these ViewHolder/View's are recycled.
+ */
+ protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position);
+
+ /**
+ * Called when this adapter has been popped off the stack and is no longer needed. Subclasses
+ * can override to do any necessary cleanup.
+ */
+ public void cleanup() {}
+}
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerController.java b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
new file mode 100644
index 0000000..7b23714
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.drawer;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.annotation.AnimRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.car.widget.PagedListView;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.widget.ProgressBar;
+
+import java.util.Stack;
+
+/**
+ * A controller that will handle the set up of the navigation drawer. It will hook up the
+ * necessary buttons for up navigation, as well as expose methods to allow for a drill down
+ * navigation.
+ */
+public class CarDrawerController {
+ /** An animation for when a user navigates into a submenu. */
+ @AnimRes
+ private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim;
+
+ /** An animation for when a user navigates up (when the back button is pressed). */
+ @AnimRes
+ private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim;
+
+ /** The amount that the drawer has been opened before its color should be switched. */
+ private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f;
+
+ /**
+ * A representation of the hierarchy of navigation being displayed in the list. The ordering of
+ * this stack is the order that the user has visited each level. When the user navigates up,
+ * the adapters are popped from this list.
+ */
+ private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>();
+
+ private final Context mContext;
+
+ private final Toolbar mToolbar;
+ private final DrawerLayout mDrawerLayout;
+ private final ActionBarDrawerToggle mDrawerToggle;
+
+ private final PagedListView mDrawerList;
+ private final ProgressBar mProgressBar;
+ private final View mDrawerContent;
+
+ /**
+ * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by
+ * {@code drawerLayout}.
+ *
+ * <p>The given {@code drawerLayout} should either have a child View that is inflated from
+ * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that
+ * layout.
+ *
+ * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity.
+ * @param drawerLayout The top-level container for the window content that shows the
+ * interactive drawer.
+ * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar}
+ * and {@code drawerLayout}.
+ */
+ public CarDrawerController(Toolbar toolbar,
+ DrawerLayout drawerLayout,
+ ActionBarDrawerToggle drawerToggle) {
+ mToolbar = toolbar;
+ mContext = drawerLayout.getContext();
+ mDrawerToggle = drawerToggle;
+ mDrawerLayout = drawerLayout;
+
+ mDrawerContent = drawerLayout.findViewById(R.id.drawer_content);
+ mDrawerList = drawerLayout.findViewById(R.id.drawer_list);
+ mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+ mProgressBar = drawerLayout.findViewById(R.id.drawer_progress);
+
+ setupDrawerToggling();
+ }
+
+ /**
+ * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of
+ * this root adapter are shown when the drawer is first opened. It is also the top-most level of
+ * navigation in the drawer.
+ *
+ * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then
+ * this method will do nothing.
+ */
+ public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) {
+ if (rootAdapter == null) {
+ return;
+ }
+
+ // The root adapter is always the last item in the stack.
+ if (mAdapterStack.size() > 0) {
+ mAdapterStack.set(0, rootAdapter);
+ } else {
+ mAdapterStack.push(rootAdapter);
+ }
+
+ setToolbarTitleFrom(rootAdapter);
+ mDrawerList.setAdapter(rootAdapter);
+ }
+
+ /**
+ * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display
+ * in the navigation drawer. The title will also be updated from the adapter.
+ *
+ * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away
+ * from this level will pop the given adapter off and surface contents of the previous adapter
+ * that was set via this method. If no such adapter exists, then the root adapter set by
+ * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead.
+ *
+ * @param adapter Adapter for next level of content in the drawer.
+ */
+ public final void pushAdapter(CarDrawerAdapter adapter) {
+ mAdapterStack.peek().setTitleChangeListener(null);
+ mAdapterStack.push(adapter);
+ setDisplayAdapter(adapter);
+ runLayoutAnimation(DRILL_DOWN_ANIM);
+ }
+
+ /** Close the drawer. */
+ public void closeDrawer() {
+ if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
+ mDrawerLayout.closeDrawer(Gravity.LEFT);
+ }
+ }
+
+ /** Opens the drawer. */
+ public void openDrawer() {
+ if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
+ mDrawerLayout.openDrawer(Gravity.LEFT);
+ }
+ }
+
+ /** Sets a listener to be notified of Drawer events. */
+ public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
+ mDrawerLayout.addDrawerListener(listener);
+ }
+
+ /** Removes a listener to be notified of Drawer events. */
+ public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
+ mDrawerLayout.removeDrawerListener(listener);
+ }
+
+ /**
+ * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true},
+ * the progress bar is displayed and the navigation list is hidden and vice versa.
+ */
+ public void showLoadingProgressBar(boolean show) {
+ mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
+ mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ /** Scroll to given position in the list. */
+ public void scrollToPosition(int position) {
+ mDrawerList.getRecyclerView().smoothScrollToPosition(position);
+ }
+
+ /**
+ * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this
+ * controller's internal Toolbar.
+ */
+ private void setToolbarTitleFrom(CarDrawerAdapter adapter) {
+ if (adapter.getTitle() == null) {
+ throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()");
+ }
+
+ mToolbar.setTitle(adapter.getTitle());
+ adapter.setTitleChangeListener(mToolbar::setTitle);
+ }
+
+ /**
+ * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer
+ * hierarchy is properly displayed.
+ */
+ private void setupDrawerToggling() {
+ mDrawerLayout.addDrawerListener(mDrawerToggle);
+ mDrawerLayout.addDrawerListener(
+ new DrawerLayout.DrawerListener() {
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {
+ // Correctly set the title and arrow colors as they are different between
+ // the open and close states.
+ updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET);
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ // If drawer is closed, revert stack/drawer to initial root state.
+ cleanupStackAndShowRoot();
+ scrollToPosition(0);
+ }
+
+ @Override
+ public void onDrawerOpened(View drawerView) {}
+
+ @Override
+ public void onDrawerStateChanged(int newState) {}
+ });
+ }
+
+ /** Sets the title and arrow color of the drawer depending on if it is open or not. */
+ private void updateTitleAndArrowColor(boolean drawerOpen) {
+ // When the drawer is open, use car_title, which resolves to appropriate color depending on
+ // day-night mode. When drawer is closed, we always use light color.
+ int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light;
+ int titleColor = mContext.getColor(titleColorResId);
+ mToolbar.setTitleTextColor(titleColor);
+ mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor);
+ }
+
+ /**
+ * Synchronizes the display of the drawer with its linked {@link DrawerLayout}.
+ *
+ * <p>This should be called from the associated Activity's
+ * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize
+ * after teh DRawerLayout's instance state has been restored, and any other time when the
+ * state may have diverged in such a way that this controller's associated
+ * {@link ActionBarDrawerToggle} had not been notified.
+ */
+ public void syncState() {
+ mDrawerToggle.syncState();
+
+ // In case we're restarting after a config change (e.g. day, night switch), set colors
+ // again. Doing it here so that Drawer state is fully synced and we know if its open or not.
+ // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout.
+ updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent));
+ }
+
+ /**
+ * Notify this controller that device configurations may have changed.
+ *
+ * <p>This method should be called from the associated Activity's
+ * {@code onConfigurationChanged()} method.
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Pass any configuration change to the drawer toggle.
+ mDrawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ /**
+ * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called
+ * when the Activity's method is called and will return {@code true} if the selection has
+ * been handled.
+ *
+ * @return {@code true} if the item processing was handled by this class.
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle home-click and see if we can navigate up in the drawer.
+ if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) {
+ return true;
+ }
+
+ // DrawerToggle gets next chance to handle up-clicks (and any other clicks).
+ return mDrawerToggle.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Sets the given adapter as the one displaying the current contents of the drawer.
+ *
+ * <p>The drawer's title will also be derived from the given adapter.
+ */
+ private void setDisplayAdapter(CarDrawerAdapter adapter) {
+ setToolbarTitleFrom(adapter);
+ // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between
+ // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts.
+ mDrawerList.getRecyclerView().setAdapter(adapter);
+ }
+
+ /**
+ * Switches to the previous level in the drawer hierarchy if the current list being displayed
+ * is not the root adapter. This is analogous to a navigate up.
+ *
+ * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise.
+ */
+ private boolean maybeHandleUpClick() {
+ // Check if already at the root level.
+ if (mAdapterStack.size() <= 1) {
+ return false;
+ }
+
+ CarDrawerAdapter adapter = mAdapterStack.pop();
+ adapter.setTitleChangeListener(null);
+ adapter.cleanup();
+ setDisplayAdapter(mAdapterStack.peek());
+ runLayoutAnimation(NAVIGATE_UP_ANIM);
+ return true;
+ }
+
+ /** Clears stack down to root adapter and switches to root adapter. */
+ private void cleanupStackAndShowRoot() {
+ while (mAdapterStack.size() > 1) {
+ CarDrawerAdapter adapter = mAdapterStack.pop();
+ adapter.setTitleChangeListener(null);
+ adapter.cleanup();
+ }
+ setDisplayAdapter(mAdapterStack.peek());
+ runLayoutAnimation(NAVIGATE_UP_ANIM);
+ }
+
+ /**
+ * Runs the given layout animation on the PagedListView. Running this animation will also
+ * refresh the contents of the list.
+ */
+ private void runLayoutAnimation(@AnimRes int animation) {
+ RecyclerView recyclerView = mDrawerList.getRecyclerView();
+ recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation));
+ recyclerView.getAdapter().notifyDataSetChanged();
+ recyclerView.scheduleLayoutAnimation();
+ }
+}
diff --git a/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
new file mode 100644
index 0000000..d707dbd
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.drawer;
+
+/**
+ * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}.
+ */
+public interface DrawerItemClickListener {
+ /**
+ * Callback when item is clicked.
+ *
+ * @param position Adapter position of the clicked item.
+ */
+ void onItemClick(int position);
+}
diff --git a/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java b/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java
new file mode 100644
index 0000000..d016b2d
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.drawer;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the
+ * {@link android.support.car.drawer.CarDrawerAdapter}.
+ */
+public class DrawerItemViewHolder extends RecyclerView.ViewHolder {
+ private final ImageView mIcon;
+ private final TextView mTitle;
+ private final TextView mText;
+ private final ImageView mEndIcon;
+
+ DrawerItemViewHolder(View view) {
+ super(view);
+ mIcon = view.findViewById(R.id.icon);
+ if (mIcon == null) {
+ throw new IllegalArgumentException("Icon view cannot be null!");
+ }
+
+ mTitle = view.findViewById(R.id.title);
+ if (mTitle == null) {
+ throw new IllegalArgumentException("Title view cannot be null!");
+ }
+
+ // Next two are optional and may be null.
+ mText = view.findViewById(R.id.text);
+ mEndIcon = view.findViewById(R.id.end_icon);
+ }
+
+ /** Returns the view that should be used to display the main icon. */
+ @NonNull
+ public ImageView getIcon() {
+ return mIcon;
+ }
+
+ /** Returns the view that will display the main title. */
+ @NonNull
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ /** Returns the view that is used for text that is smaller than the title text. */
+ @Nullable
+ public TextView getText() {
+ return mText;
+ }
+
+ /** Returns the icon that is displayed at the end of the view. */
+ @Nullable
+ public ImageView getEndIcon() {
+ return mEndIcon;
+ }
+
+ /**
+ * Sets the listener that will be notified when the view held by this ViewHolder has been
+ * clicked. Passing {@code null} will clear any previously set listeners.
+ */
+ void setItemClickListener(@Nullable DrawerItemClickListener listener) {
+ itemView.setOnClickListener(listener != null
+ ? v -> listener.onItemClick(getAdapterPosition())
+ : null);
+ }
+}
diff --git a/car/src/main/java/android/support/car/utils/ColumnCalculator.java b/car/src/main/java/android/support/car/utils/ColumnCalculator.java
new file mode 100644
index 0000000..fa5dd43
--- /dev/null
+++ b/car/src/main/java/android/support/car/utils/ColumnCalculator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.utils;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.car.R;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Utility class that calculates the size of the columns that will fit on the screen. A column's
+ * width is determined by the size of the margins and gutters (space between the columns) that fit
+ * on-screen.
+ *
+ * <p>Refer to the appropriate dimens and integers for the size of the margins and number of
+ * columns.
+ */
+public class ColumnCalculator {
+ private static final String TAG = "ColumnCalculator";
+
+ private static ColumnCalculator sInstance;
+ private static int sScreenWidth;
+
+ private int mNumOfColumns;
+ private int mNumOfGutters;
+ private int mColumnWidth;
+ private int mGutterSize;
+
+ /**
+ * Gets an instance of the {@link ColumnCalculator}. If this is the first time that this
+ * method has been called, then the given {@link Context} will be used to retrieve resources.
+ *
+ * @param context The current calling Context.
+ * @return An instance of {@link ColumnCalculator}.
+ */
+ public static ColumnCalculator getInstance(Context context) {
+ if (sInstance == null) {
+ WindowManager windowManager = (WindowManager) context.getSystemService(
+ Context.WINDOW_SERVICE);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+ sScreenWidth = displayMetrics.widthPixels;
+
+ sInstance = new ColumnCalculator(context);
+ }
+
+ return sInstance;
+ }
+
+ private ColumnCalculator(Context context) {
+ Resources res = context.getResources();
+ int marginSize = res.getDimensionPixelSize(R.dimen.car_margin);
+ mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size);
+ mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d",
+ marginSize, mNumOfColumns, mGutterSize));
+ }
+
+ // The gutters appear between each column. As a result, the number of gutters is one less
+ // than the number of columns.
+ mNumOfGutters = mNumOfColumns - 1;
+
+ // Determine the spacing that is allowed to be filled by the columns by subtracting margins
+ // on both size of the screen and the space taken up by the gutters.
+ int spaceForColumns = sScreenWidth - (2 * marginSize) - (mNumOfGutters * mGutterSize);
+
+ mColumnWidth = spaceForColumns / mNumOfColumns;
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mColumnWidth: " + mColumnWidth);
+ }
+ }
+
+ /**
+ * Returns the total number of columns that fit on the current screen.
+ *
+ * @return The total number of columns that fit on the screen.
+ */
+ public int getNumOfColumns() {
+ return mNumOfColumns;
+ }
+
+ /**
+ * Returns the size in pixels of each column. The column width is determined by the size of the
+ * screen divided by the number of columns, size of gutters and margins.
+ *
+ * @return The width of a single column in pixels.
+ */
+ public int getColumnWidth() {
+ return mColumnWidth;
+ }
+
+ /**
+ * Returns the total number of gutters that fit on screen. A gutter is the space between each
+ * column. This value is always one less than the number of columns.
+ *
+ * @return The number of gutters on screen.
+ */
+ public int getNumOfGutters() {
+ return mNumOfGutters;
+ }
+
+ /**
+ * Returns the size of each gutter in pixels. A gutter is the space between each column.
+ *
+ * @return The size of a single gutter in pixels.
+ */
+ public int getGutterSize() {
+ return mGutterSize;
+ }
+
+ /**
+ * Returns the size in pixels for the given number of columns. This value takes into account
+ * the size of the gutter between the columns as well. For example, for a column span of four,
+ * the size returned is the sum of four columns and three gutters.
+ *
+ * @return The size in pixels for a given column span.
+ */
+ public int getSizeForColumnSpan(int columnSpan) {
+ int gutterSpan = columnSpan - 1;
+ return columnSpan * mColumnWidth + gutterSpan * mGutterSize;
+ }
+}
diff --git a/car/src/main/java/android/support/car/widget/CarItemAnimator.java b/car/src/main/java/android/support/car/widget/CarItemAnimator.java
new file mode 100644
index 0000000..ef22c48
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarItemAnimator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
+
+/** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */
+public class CarItemAnimator extends DefaultItemAnimator {
+
+ private final PagedLayoutManager mLayoutManager;
+
+ public CarItemAnimator(PagedLayoutManager layoutManager) {
+ mLayoutManager = layoutManager;
+ }
+
+ @Override
+ public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+ RecyclerView.ViewHolder newHolder,
+ int fromX,
+ int fromY,
+ int toX,
+ int toY) {
+ // The default behavior will cross fade the old view and the new one. However, if we
+ // have a card on a colored background, it will make it appear as if a changing card
+ // fades in and out.
+ float alpha = 0f;
+ if (newHolder != null) {
+ alpha = newHolder.itemView.getAlpha();
+ }
+ boolean ret = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
+ if (newHolder != null) {
+ newHolder.itemView.setAlpha(alpha);
+ }
+ return ret;
+ }
+
+ @Override
+ public void onMoveFinished(RecyclerView.ViewHolder item) {
+ // The item animator uses translation heavily internally. However, we also use translation
+ // to create the paging affect. When an item's move is animated, it will mess up the
+ // translation we have set on it so we must re-offset the rows once the animations finish.
+
+ // isRunning(ItemAnimationFinishedListener) is the awkward API used to determine when all
+ // animations have finished.
+ isRunning(mFinishedListener);
+ }
+
+ private final ItemAnimatorFinishedListener mFinishedListener =
+ new ItemAnimatorFinishedListener() {
+ @Override
+ public void onAnimationsFinished() {
+ mLayoutManager.offsetRows();
+ }
+ };
+}
diff --git a/car/src/main/java/android/support/car/widget/CarRecyclerView.java b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
new file mode 100644
index 0000000..bb9cb71
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Custom {@link RecyclerView} that helps {@link PagedLayoutManager} properly fling and paginate.
+ *
+ * <p>It also has the ability to fade children as they scroll off screen that can be set with {@link
+ * #setFadeLastItem(boolean)}.
+ */
+public class CarRecyclerView extends RecyclerView {
+ private boolean mFadeLastItem;
+ /**
+ * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be
+ * called. However, we want to make sure that the list still snaps to the next page when this
+ * happens.
+ */
+ private boolean mWasFlingCalledForGesture;
+
+ public CarRecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public CarRecyclerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFocusableInTouchMode(false);
+ setFocusable(false);
+ }
+
+ @Override
+ public boolean fling(int velocityX, int velocityY) {
+ mWasFlingCalledForGesture = true;
+ return ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ // We want the parent to handle all touch events. There's a lot going on there,
+ // and there is no reason to overwrite that functionality. If we do, bad things will happen.
+ final boolean ret = super.onTouchEvent(e);
+
+ int action = e.getActionMasked();
+ if (action == MotionEvent.ACTION_UP) {
+ if (!mWasFlingCalledForGesture) {
+ ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, 0);
+ }
+ mWasFlingCalledForGesture = false;
+ }
+
+ return ret;
+ }
+
+ @Override
+ public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
+ if (mFadeLastItem) {
+ float onScreen = 1f;
+ if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) {
+ onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight();
+ } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) {
+ onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight();
+ }
+ float alpha = 1 - (1 - onScreen) * (1 - onScreen);
+ fadeChild(child, alpha);
+ }
+
+ return super.drawChild(canvas, child, drawingTime);
+ }
+
+ public void setFadeLastItem(boolean fadeLastItem) {
+ mFadeLastItem = fadeLastItem;
+ }
+
+ /**
+ * Scrolls the contents of this {@link CarRecyclerView} up one page. A page is defined as the
+ * number of items that fit completely on the screen.
+ */
+ public void pageUp() {
+ PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager();
+ int pageUpPosition = lm.getPageUpPosition();
+ if (pageUpPosition == -1) {
+ return;
+ }
+
+ smoothScrollToPosition(pageUpPosition);
+ }
+
+ /**
+ * Scrolls the contents of this {@link CarRecyclerView} down one page. A page is defined as the
+ * number of items that fit completely on the screen.
+ */
+ public void pageDown() {
+ PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager();
+ int pageDownPosition = lm.getPageDownPosition();
+ if (pageDownPosition == -1) {
+ return;
+ }
+
+ smoothScrollToPosition(pageDownPosition);
+ }
+
+ /**
+ * Fades child by alpha. If child is a {@link ViewGroup} then it will recursively fade its
+ * children instead.
+ */
+ private void fadeChild(@NonNull View child, float alpha) {
+ if (child instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup) child;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ fadeChild(vg.getChildAt(i), alpha);
+ }
+ } else {
+ child.setAlpha(alpha);
+ }
+ }
+}
diff --git a/car/src/main/java/android/support/car/widget/ColumnCardView.java b/car/src/main/java/android/support/car/widget/ColumnCardView.java
new file mode 100644
index 0000000..06f8553
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/ColumnCardView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.car.R;
+import android.support.car.utils.ColumnCalculator;
+import android.support.v7.widget.CardView;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * A {@link CardView} whose width can be specified by the number of columns that it will span.
+ *
+ * <p>The {@code ColumnCardView} works similarly to a regular {@link CardView}, except that
+ * its {@code layout_width} attribute is always ignored. Instead, its width is automatically
+ * calculated based on a specified {@code columnSpan} attribute. Alternatively, a user can call
+ * {@link #setColumnSpan(int)}. If no column span is given, the {@code ColumnCardView} will have
+ * a default span value that it uses.
+ *
+ * <pre>
+ * <android.support.car.widget.ColumnCardView
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * app:columnSpan="4" />
+ * </pre>
+ *
+ * @see ColumnCalculator
+ */
+public final class ColumnCardView extends CardView {
+ private static final String TAG = "ColumnCardView";
+
+ private ColumnCalculator mColumnCalculator;
+ private int mColumnSpan;
+
+ public ColumnCardView(Context context) {
+ super(context);
+ init(context, null, 0 /* defStyleAttrs */);
+ }
+
+ public ColumnCardView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0 /* defStyleAttrs */);
+ }
+
+ public ColumnCardView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttrs) {
+ mColumnCalculator = ColumnCalculator.getInstance(context);
+
+ int defaultColumnSpan = getResources().getInteger(
+ R.integer.column_card_default_column_span);
+
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColumnCardView,
+ defStyleAttrs, 0 /* defStyleRes */);
+ mColumnSpan = ta.getInteger(R.styleable.ColumnCardView_columnSpan, defaultColumnSpan);
+ ta.recycle();
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Column span: " + mColumnSpan);
+ }
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Override any specified width so that the width is one that is calculated based on
+ // column and gutter span.
+ int width = mColumnCalculator.getSizeForColumnSpan(mColumnSpan);
+ super.onMeasure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ heightMeasureSpec);
+ }
+
+ /**
+ * Sets the number of columns that this {@code ColumnCardView} will span. The given span is
+ * ignored if it is less than 0 or greater than the number of columns that fit on screen.
+ *
+ * @param columnSpan The number of columns this {@code ColumnCardView} will span across.
+ */
+ public void setColumnSpan(int columnSpan) {
+ if (columnSpan <= 0 || columnSpan > mColumnCalculator.getNumOfColumns()) {
+ return;
+ }
+
+ mColumnSpan = columnSpan;
+ requestLayout();
+ }
+
+ /**
+ * Returns the currently number of columns that this {@code ColumnCardView} spans.
+ *
+ * @return The number of columns this {@code ColumnCardView} spans across.
+ */
+ public int getColumnSpan() {
+ return mColumnSpan;
+ }
+}
diff --git a/car/src/main/java/android/support/car/widget/DayNightStyle.java b/car/src/main/java/android/support/car/widget/DayNightStyle.java
new file mode 100644
index 0000000..ff5a1b3
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/DayNightStyle.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.support.annotation.IntDef;
+
+/**
+ * Specifies how the system UI should respond to day/night mode events.
+ *
+ * <p>By default, the Android Auto system UI assumes the app content background is light during the
+ * day and dark during the night. The system UI updates the foreground color (such as status bar
+ * icon colors) to be dark during day mode and light during night mode. By setting the
+ * DayNightStyle, the app can specify how the system should respond to a day/night mode event. For
+ * example, if the app has a dark content background for both day and night time, the app can tell
+ * the system to use {@link #FORCE_NIGHT} style so the foreground color is locked to light color for
+ * both cases.
+ *
+ * <p>Note: Not all system UI elements can be customized with a DayNightStyle.
+ */
+@IntDef({
+ DayNightStyle.AUTO,
+ DayNightStyle.AUTO_INVERSE,
+ DayNightStyle.FORCE_NIGHT,
+ DayNightStyle.FORCE_DAY,
+})
+public @interface DayNightStyle {
+ /**
+ * Sets the foreground color to be automatically changed based on day/night mode, assuming the
+ * app content background is light during the day and dark during the night.
+ *
+ * <p>This is the default behavior.
+ */
+ int AUTO = 0;
+
+ /**
+ * Sets the foreground color to be automatically changed based on day/night mode, assuming the
+ * app content background is dark during the day and light during the night.
+ */
+ int AUTO_INVERSE = 1;
+
+ /**
+ * Sets the foreground color to be locked to the night version, which assumes the app content
+ * background is always dark during both day and night.
+ */
+ int FORCE_NIGHT = 2;
+
+ /**
+ * Sets the foreground color to be locked to the day version, which assumes the app content
+ * background is always light during both day and night.
+ */
+ int FORCE_DAY = 3;
+}
diff --git a/car/src/main/java/android/support/car/widget/PagedLayoutManager.java b/car/src/main/java/android/support/car/widget/PagedLayoutManager.java
new file mode 100644
index 0000000..c4f469a
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedLayoutManager.java
@@ -0,0 +1,1687 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.car.R;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Recycler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that
+ * it has a few tricks up its sleeve.
+ *
+ * <ol>
+ * <li>In a normal ListView, when views reach the top of the list, they are clipped. In
+ * PagedLayoutManager, views have the option of flying off of the top of the screen as the
+ * next row settles in to place. This functionality can be enabled or disabled with
+ * {@link #setOffsetRows(boolean)}.
+ * <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle on the
+ * next page.
+ * <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that the
+ * last page can be properly aligned.
+ * </ol>
+ *
+ * This LayoutManger should be used with {@link CarRecyclerView}.
+ */
+public class PagedLayoutManager extends RecyclerView.LayoutManager {
+ private static final String TAG = "PagedLayoutManager";
+
+ /**
+ * Any fling below the threshold will just scroll to the top fully visible row. The units is
+ * whatever {@link android.widget.Scroller} would return.
+ *
+ * <p>A reasonable value is ~200
+ *
+ * <p>This can be disabled by setting the threshold to -1.
+ */
+ private static final int FLING_THRESHOLD_TO_PAGINATE = -1;
+
+ /**
+ * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row.
+ *
+ * <p>A reasonable value is 15.
+ *
+ * <p>This can be disabled by setting the distance to -1.
+ */
+ private static final int DRAG_DISTANCE_TO_PAGINATE = -1;
+
+ /**
+ * If you scroll really quickly, you can hit the end of the laid out rows before Android has a
+ * chance to layout more. To help counter this, we can layout a number of extra rows past
+ * wherever the focus is if necessary.
+ */
+ private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2;
+
+ /**
+ * Scroll bar calculation is a bit complicated. This basically defines the granularity we want
+ * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement.
+ * Setting it too big will risk an overflow (although there is no performance impact). Ideally
+ * we want to set this higher than the height of our list view. We can't use our list view
+ * height directly though because we might run into situations where getHeight() returns 0,
+ * for example, when the view is not yet measured.
+ */
+ private static final int SCROLL_RANGE = 1000;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({BEFORE, AFTER})
+ private @interface LayoutDirection {}
+
+ private static final int BEFORE = 0;
+ private static final int AFTER = 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE})
+ public @interface RowOffsetMode {}
+
+ public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0;
+ public static final int ROW_OFFSET_MODE_PAGE = 1;
+
+ private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2);
+ private final Context mContext;
+
+ /** Determines whether or not rows will be offset as they slide off screen * */
+ private boolean mOffsetRows;
+
+ /** Determines whether rows will be offset individually or a page at a time * */
+ @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE;
+
+ /**
+ * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the
+ * scroll state to be used anywhere.
+ */
+ private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
+
+ /** Used to inspect the current scroll state to help with the various calculations. */
+ private CarSmoothScroller mSmoothScroller;
+
+ private PagedListView.OnScrollListener mOnScrollListener;
+
+ /** The distance that the list has actually scrolled in the most recent drag gesture. */
+ private int mLastDragDistance = 0;
+
+ /** {@code True} if the current drag was limited/capped because it was at some boundary. */
+ private boolean mReachedLimitOfDrag;
+
+ /** The index of the first item on the current page. */
+ private int mAnchorPageBreakPosition = 0;
+
+ /** The index of the first item on the previous page. */
+ private int mUpperPageBreakPosition = -1;
+
+ /** The index of the first item on the next page. */
+ private int mLowerPageBreakPosition = -1;
+
+ /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. */
+ private int mLastChildPositionToRequestFocus = -1;
+
+ private int mSampleViewHeight = -1;
+
+ /** Used for onPageUp and onPageDown */
+ private int mViewsPerPage = 1;
+
+ private int mCurrentPage = 0;
+
+ private static final int MAX_ANIMATIONS_IN_CACHE = 30;
+ /**
+ * Cache of TranslateAnimation per child view. These are needed since using a single animation
+ * for all children doesn't apply the animation effect multiple times. Key = the view the
+ * animation will transform.
+ */
+ private LruCache<View, TranslateAnimation> mFlyOffscreenAnimations;
+
+ /** Set the anchor to the following position on the next layout pass. */
+ private int mPendingScrollPosition = -1;
+
+ public PagedLayoutManager(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return true;
+ }
+
+ /**
+ * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should:
+ *
+ * <ol>
+ * <li>Check the current views to get the current state of affairs
+ * <li>Detach all views from the window (a lightweight operation) so that rows not re-added
+ * will be removed after onLayoutChildren.
+ * <li>Re-add rows as necessary.
+ * </ol>
+ *
+ * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)
+ */
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ /*
+ * The anchor view is the first fully visible view on screen at the beginning of
+ * onLayoutChildren (or 0 if there is none). This row will be laid out first. After that,
+ * layoutNextRow will layout rows above and below it until the boundaries of what should be
+ * laid out have been reached. See shouldLayoutNextRow(View, int) for more info.
+ */
+ int anchorPosition = 0;
+ int anchorTop = -1;
+ if (mPendingScrollPosition == -1) {
+ View anchor = getFirstFullyVisibleChild();
+ if (anchor != null) {
+ anchorPosition = getPosition(anchor);
+ anchorTop = getDecoratedTop(anchor);
+ }
+ } else {
+ anchorPosition = mPendingScrollPosition;
+ mPendingScrollPosition = -1;
+ mAnchorPageBreakPosition = anchorPosition;
+ mUpperPageBreakPosition = -1;
+ mLowerPageBreakPosition = -1;
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(
+ TAG,
+ String.format(
+ ":: onLayoutChildren anchorPosition:%s, anchorTop:%s,"
+ + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s,"
+ + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s",
+ anchorPosition,
+ anchorTop,
+ mPendingScrollPosition,
+ mAnchorPageBreakPosition,
+ mUpperPageBreakPosition,
+ mLowerPageBreakPosition));
+ }
+
+ /*
+ * Detach all attached view for 2 reasons:
+ *
+ * 1) So that views are put in the scrap heap. This enables us to call {@link
+ * RecyclerView.Recycler#getViewForPosition(int)} which will either return one of these
+ * detached views if it is in the scrap heap, one from the recycled pool (will only call
+ * onBind in the adapter), or create an entirely new row if needed (will call onCreate
+ * and onBind in the adapter).
+ * 2) So that views are automatically removed if they are not manually re-added.
+ */
+ detachAndScrapAttachedViews(recycler);
+
+ /*
+ * Layout the views recursively.
+ *
+ * It's possible that this re-layout is triggered because an item gets removed. If the
+ * anchor view is at the end of the list, the anchor view position will be bigger than the
+ * number of available items. Correct that, and only start the layout if the anchor
+ * position is valid.
+ */
+ anchorPosition = Math.min(anchorPosition, getItemCount() - 1);
+ if (anchorPosition >= 0) {
+ View anchor = layoutAnchor(recycler, anchorPosition, anchorTop);
+ View adjacentRow = anchor;
+ while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
+ adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
+ }
+ adjacentRow = anchor;
+ while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
+ adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
+ }
+ }
+
+ updatePageBreakPositions();
+ offsetRows();
+
+ if (Log.isLoggable(TAG, Log.VERBOSE) && getChildCount() > 1) {
+ Log.v(TAG, "Currently showing "
+ + getChildCount()
+ + " views "
+ + getPosition(getChildAt(0))
+ + " to "
+ + getPosition(getChildAt(getChildCount() - 1))
+ + " anchor "
+ + anchorPosition);
+ }
+ // Should be at least 1
+ mViewsPerPage =
+ Math.max(getLastFullyVisibleChildIndex() + 1 - getFirstFullyVisibleChildIndex(), 1);
+ mCurrentPage = getFirstFullyVisibleChildPosition() / mViewsPerPage;
+ Log.v(TAG, "viewsPerPage " + mViewsPerPage);
+ }
+
+ /**
+ * scrollVerticallyBy does the work of what should happen when the list scrolls in addition to
+ * handling cases where the list hits the end. It should be lighter weight than
+ * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list
+ * and removes views that have gone out of bounds and lays out new ones that scroll in.
+ *
+ * @param dy The amount that the list is supposed to scroll. > 0 means the list is scrolling
+ * down. < 0 means the list is scrolling up.
+ * @param recycler The recycler that enables views to be reused or created as they scroll in.
+ * @param state Various information about the current state of affairs.
+ * @return The amount the list actually scrolled.
+ * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State)
+ */
+ @Override
+ public int scrollVerticallyBy(
+ int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) {
+ // If the list is empty, we can prevent the overscroll glow from showing by just
+ // telling RecycerView that we scrolled.
+ if (getItemCount() == 0) {
+ return dy;
+ }
+
+ // Prevent redundant computations if there is definitely nowhere to scroll to.
+ if (getChildCount() <= 1 || dy == 0) {
+ mReachedLimitOfDrag = true;
+ return 0;
+ }
+
+ View firstChild = getChildAt(0);
+ if (firstChild == null) {
+ mReachedLimitOfDrag = true;
+ return 0;
+ }
+ int firstChildPosition = getPosition(firstChild);
+ RecyclerView.LayoutParams firstChildParams = getParams(firstChild);
+ int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin;
+
+ View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex());
+ if (lastFullyVisibleView == null) {
+ mReachedLimitOfDrag = true;
+ return 0;
+ }
+ boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1;
+
+ View firstFullyVisibleChild = getFirstFullyVisibleChild();
+ if (firstFullyVisibleChild == null) {
+ mReachedLimitOfDrag = true;
+ return 0;
+ }
+ int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild);
+ RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild);
+ int topRemainingSpace =
+ getDecoratedTop(firstFullyVisibleChild)
+ - firstFullyVisibleChildParams.topMargin
+ - getPaddingTop();
+
+ if (isLastViewVisible
+ && firstFullyVisiblePosition == mAnchorPageBreakPosition
+ && dy > topRemainingSpace
+ && dy > 0) {
+ // Prevent dragging down more than 1 page. As a side effect, this also prevents you
+ // from dragging past the bottom because if you are on the second to last page, it
+ // prevents you from dragging past the last page.
+ dy = topRemainingSpace;
+ mReachedLimitOfDrag = true;
+ } else if (dy < 0
+ && firstChildPosition == 0
+ && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) {
+ // Prevent scrolling past the beginning
+ dy = firstChildTopWithMargin - getPaddingTop();
+ mReachedLimitOfDrag = true;
+ } else {
+ mReachedLimitOfDrag = false;
+ }
+
+ boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING;
+ if (isDragging) {
+ mLastDragDistance += dy;
+ }
+ // We offset by -dy because the views translate in the opposite direction that the
+ // list scrolls (think about it.)
+ offsetChildrenVertical(-dy);
+
+ // The last item in the layout should never scroll above the viewport
+ View view = getChildAt(getChildCount() - 1);
+ if (view.getTop() < 0) {
+ view.setTop(0);
+ }
+
+ // This is the meat of this function. We remove views on the trailing edge of the scroll
+ // and add views at the leading edge as necessary.
+ View adjacentRow;
+ if (dy > 0) {
+ recycleChildrenFromStart(recycler);
+ adjacentRow = getChildAt(getChildCount() - 1);
+ while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
+ adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
+ }
+ } else {
+ recycleChildrenFromEnd(recycler);
+ adjacentRow = getChildAt(0);
+ while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
+ adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
+ }
+ }
+ // Now that the correct views are laid out, offset rows as necessary so we can do whatever
+ // fancy animation we want such as having the top view fly off the screen as the next one
+ // settles in to place.
+ updatePageBreakPositions();
+ offsetRows();
+
+ if (getChildCount() > 1) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(
+ TAG,
+ String.format(
+ "Currently showing %d views (%d to %d)",
+ getChildCount(),
+ getPosition(getChildAt(0)),
+ getPosition(getChildAt(getChildCount() - 1))));
+ }
+ }
+ updatePagedState();
+ return dy;
+ }
+
+ private void updatePagedState() {
+ int page = getFirstFullyVisibleChildPosition() / mViewsPerPage;
+ if (mOnScrollListener != null) {
+ if (page > mCurrentPage) {
+ mOnScrollListener.onPageDown();
+ } else if (page < mCurrentPage) {
+ mOnScrollListener.onPageUp();
+ }
+ }
+ mCurrentPage = page;
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ mPendingScrollPosition = position;
+ requestLayout();
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+ int position) {
+ /*
+ * startSmoothScroll will handle stopping the old one if there is one. We only keep a copy
+ * of it to handle the translation of rows as they slide off the screen in
+ * offsetRowsWithPageBreak().
+ */
+ mSmoothScroller = new CarSmoothScroller(mContext, position);
+ mSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(mSmoothScroller);
+ }
+
+ /** Miscellaneous bookkeeping. */
+ @Override
+ public void onScrollStateChanged(int state) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, ":: onScrollStateChanged " + state);
+ }
+ if (state == RecyclerView.SCROLL_STATE_IDLE) {
+ // If the focused view is off screen, give focus to one that is.
+ // If the first fully visible view is first in the list, focus the first item.
+ // Otherwise, focus the second so that you have the first item as scrolling context.
+ View focusedChild = getFocusedChild();
+ if (focusedChild != null
+ && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom()
+ || getDecoratedBottom(focusedChild) <= getPaddingTop())) {
+ focusedChild.clearFocus();
+ requestLayout();
+ }
+
+ } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) {
+ mLastDragDistance = 0;
+ }
+
+ if (state != RecyclerView.SCROLL_STATE_SETTLING) {
+ mSmoothScroller = null;
+ }
+
+ mScrollState = state;
+ updatePageBreakPositions();
+ }
+
+ @Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ super.onItemsChanged(recyclerView);
+ // When item changed, our sample view height is no longer accurate, and need to be
+ // recomputed.
+ mSampleViewHeight = -1;
+ }
+
+ /**
+ * Gives us the opportunity to override the order of the focused views. By default, it will just
+ * go from top to bottom. However, if there is no focused views, we take over the logic and
+ * start the focused views from the middle of what is visible and move from there until the
+ * end of the laid out views in the specified direction.
+ */
+ @Override
+ public boolean onAddFocusables(
+ RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
+ View focusedChild = getFocusedChild();
+ if (focusedChild != null) {
+ // If there is a view that already has focus, we can just return false and the normal
+ // Android addFocusables will work fine.
+ return false;
+ }
+
+ // Now we know that there isn't a focused view. We need to set up focusables such that
+ // instead of just focusing the first item that has been laid out, it focuses starting
+ // from a visible item.
+
+ int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+ if (firstFullyVisibleChildIndex == -1) {
+ // Somehow there is a focused view but there is no fully visible view. There shouldn't
+ // be a way for this to happen but we'd better stop here and return instead of
+ // continuing on with -1.
+ Log.w(TAG, "There is a focused child but no first fully visible child.");
+ return false;
+ }
+ View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex);
+ int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild);
+
+ int firstFocusableChildIndex = firstFullyVisibleChildIndex;
+ if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) {
+ // We are somewhere in the middle of the list. Instead of starting focus on the first
+ // item, start focus on the second item to give some context that we aren't at
+ // the beginning.
+ firstFocusableChildIndex++;
+ }
+
+ if (direction == View.FOCUS_FORWARD) {
+ // Iterate from the first focusable view to the end.
+ for (int i = firstFocusableChildIndex; i < getChildCount(); i++) {
+ views.add(getChildAt(i));
+ }
+ return true;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ // Iterate from the first focusable view to the beginning.
+ for (int i = firstFocusableChildIndex; i >= 0; i--) {
+ views.add(getChildAt(i));
+ }
+ return true;
+ } else if (direction == View.FOCUS_DOWN) {
+ // Framework calls onAddFocusables with FOCUS_DOWN direction when the focus is first
+ // gained. Thereafter, it calls onAddFocusables with FOCUS_FORWARD or FOCUS_BACKWARD.
+ // First we try to put the focus back on the last focused item, if it is visible
+ int lastFocusedVisibleChildIndex = getLastFocusedChildIndexIfVisible();
+ if (lastFocusedVisibleChildIndex != -1) {
+ views.add(getChildAt(lastFocusedVisibleChildIndex));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public View onFocusSearchFailed(
+ View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ // This doesn't seem to get called the way focus is handled in gearhead...
+ return null;
+ }
+
+ /**
+ * This is the function that decides where to scroll to when a new view is focused. You can get
+ * the position of the currently focused child through the child parameter. Once you have that,
+ * determine where to smooth scroll to and scroll there.
+ *
+ * @param parent The RecyclerView hosting this LayoutManager
+ * @param state Current state of RecyclerView
+ * @param child Direct child of the RecyclerView containing the newly focused view
+ * @param focused The newly focused view. This may be the same view as child or it may be null
+ * @return {@code true} if the default scroll behavior should be suppressed
+ */
+ @Override
+ public boolean onRequestChildFocus(
+ RecyclerView parent, RecyclerView.State state, View child, View focused) {
+ if (child == null) {
+ Log.w(TAG, "onRequestChildFocus with a null child!");
+ return true;
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child,
+ focused));
+ }
+
+ return onRequestChildFocusMarioStyle(parent, child);
+ }
+
+ /**
+ * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar
+ * reaches the bottom of the screen when the last item is fully visible. This is because there
+ * are multiple points that could be considered the bottom since the last item can scroll past
+ * the bottom edge of the screen.
+ *
+ * <p>To find the extent, we divide the number of items that can fit on screen by the number of
+ * items in total.
+ */
+ @Override
+ public int computeVerticalScrollExtent(RecyclerView.State state) {
+ if (getChildCount() <= 1) {
+ return 0;
+ }
+
+ int sampleViewHeight = getSampleViewHeight();
+ int availableHeight = getAvailableHeight();
+ int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
+
+ if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) {
+ return SCROLL_RANGE;
+ } else {
+ return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount();
+ }
+ }
+
+ /**
+ * The scrolling offset is calculated by determining what position is at the top of the list.
+ * However, instead of using fixed integer positions for each row, the scroll position is
+ * factored in and the position is recalculated as a float that takes in to account the
+ * current scroll state. This results in a smooth animation for the scrollbar when the user
+ * scrolls the list.
+ */
+ @Override
+ public int computeVerticalScrollOffset(RecyclerView.State state) {
+ View firstChild = getFirstFullyVisibleChild();
+ if (firstChild == null) {
+ return 0;
+ }
+
+ RecyclerView.LayoutParams params = getParams(firstChild);
+ int firstChildPosition = getPosition(firstChild);
+ float previousChildHieght = (float) (getDecoratedMeasuredHeight(firstChild)
+ + params.topMargin + params.bottomMargin);
+
+ // Assume the previous view is the same height as the current one.
+ float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin)
+ / previousChildHieght;
+ // If the previous view is actually larger than the current one then this the percent
+ // can be greater than 1.
+ percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1);
+
+ float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing;
+
+ int sampleViewHeight = getSampleViewHeight();
+ int availableHeight = getAvailableHeight();
+ int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
+ int positionWhenLastItemIsVisible =
+ state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen;
+
+ if (positionWhenLastItemIsVisible <= 0) {
+ return 0;
+ }
+
+ if (currentPosition >= positionWhenLastItemIsVisible) {
+ return SCROLL_RANGE;
+ }
+
+ return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible);
+ }
+
+ /**
+ * The range of the scrollbar can be understood as the granularity of how we want the scrollbar
+ * to scroll.
+ */
+ @Override
+ public int computeVerticalScrollRange(RecyclerView.State state) {
+ return SCROLL_RANGE;
+ }
+
+ @Override
+ public void onAttachedToWindow(RecyclerView view) {
+ super.onAttachedToWindow(view);
+ // The purpose of calling this is so that any animation offsets are re-applied. These are
+ // cleared in View.onDetachedFromWindow().
+ // This fixes b/27672379
+ updatePageBreakPositions();
+ offsetRows();
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView recyclerView, Recycler recycler) {
+ super.onDetachedFromWindow(recyclerView, recycler);
+ }
+
+ /**
+ * @return The first view that starts on screen. It assumes that it fully fits on the screen
+ * though. If the first fully visible child is also taller than the screen then it will
+ * still be returned. However, since the LayoutManager snaps to view starts, having a row
+ * that tall would lead to a broken experience anyways.
+ */
+ public int getFirstFullyVisibleChildIndex() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ RecyclerView.LayoutParams params = getParams(child);
+ if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return The position of first visible child in the list. -1 will be returned if there is no
+ * child.
+ */
+ public int getFirstFullyVisibleChildPosition() {
+ View child = getFirstFullyVisibleChild();
+ if (child == null) {
+ return -1;
+ }
+ return getPosition(child);
+ }
+
+ /**
+ * @return The position of last visible child in the list. -1 will be returned if there is no
+ * child.
+ */
+ public int getLastFullyVisibleChildPosition() {
+ View child = getLastFullyVisibleChild();
+ if (child == null) {
+ return -1;
+ }
+ return getPosition(child);
+ }
+
+ /** @return The first View that is completely visible on-screen. */
+ public View getFirstFullyVisibleChild() {
+ int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+ View firstChild = null;
+ if (firstFullyVisibleChildIndex != -1) {
+ firstChild = getChildAt(firstFullyVisibleChildIndex);
+ }
+ return firstChild;
+ }
+
+ /** @return The last View that is completely visible on-screen. */
+ public View getLastFullyVisibleChild() {
+ int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
+ View lastChild = null;
+ if (lastFullyVisibleChildIndex != -1) {
+ lastChild = getChildAt(lastFullyVisibleChildIndex);
+ }
+ return lastChild;
+ }
+
+ /**
+ * @return The last view that ends on screen. It assumes that the start is also on screen
+ * though. If the last fully visible child is also taller than the screen then it will
+ * still be returned. However, since the LayoutManager snaps to view starts, having a row
+ * that tall would lead to a broken experience anyways.
+ */
+ public int getLastFullyVisibleChildIndex() {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ RecyclerView.LayoutParams params = getParams(child);
+ int childBottom = getDecoratedBottom(child) + params.bottomMargin;
+ int listBottom = getHeight() - getPaddingBottom();
+ if (childBottom <= listBottom) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the index of the child in the list that was last focused and is currently visible to
+ * the user. If no child is found, returns -1.
+ */
+ public int getLastFocusedChildIndexIfVisible() {
+ if (mLastChildPositionToRequestFocus == -1) {
+ return -1;
+ }
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (getPosition(child) == mLastChildPositionToRequestFocus) {
+ RecyclerView.LayoutParams params = getParams(child);
+ int childBottom = getDecoratedBottom(child) + params.bottomMargin;
+ int listBottom = getHeight() - getPaddingBottom();
+ if (childBottom <= listBottom) {
+ return i;
+ }
+ break;
+ }
+ }
+ return -1;
+ }
+
+ /** @return Whether or not the first view is fully visible. */
+ public boolean isAtTop() {
+ // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views
+ // and also means that the list is at the top.
+ return getFirstFullyVisibleChildIndex() <= 0;
+ }
+
+ /** @return Whether or not the last view is fully visible. */
+ public boolean isAtBottom() {
+ int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
+ if (lastFullyVisibleChildIndex == -1) {
+ return true;
+ }
+ View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex);
+ return getPosition(lastFullyVisibleChild) == getItemCount() - 1;
+ }
+
+ /**
+ * Sets whether or not the rows have an offset animation when it scrolls off-screen. The type
+ * of offset is determined by {@link #setRowOffsetMode(int)}.
+ *
+ * <p>A row being offset means that when they reach the top of the screen, the row is flung off
+ * respectively to the rest of the list. This creates a gap between the offset row(s) and the
+ * list.
+ *
+ * @param offsetRows {@code true} if the rows should be offset.
+ */
+ public void setOffsetRows(boolean offsetRows) {
+ mOffsetRows = offsetRows;
+ if (offsetRows) {
+ // Card animation offsets are only needed when we use the flying off the screen effect
+ if (mFlyOffscreenAnimations == null) {
+ mFlyOffscreenAnimations = new LruCache<>(MAX_ANIMATIONS_IN_CACHE);
+ }
+ offsetRows();
+ } else {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ setCardFlyingEffectOffset(getChildAt(i), 0);
+ }
+ mFlyOffscreenAnimations = null;
+ }
+ }
+
+ /**
+ * Sets the manner of offsetting the rows when they are scrolled off-screen. The rows are either
+ * offset individually or the entire page being scrolled off is offset.
+ *
+ * @param mode One of {@link #ROW_OFFSET_MODE_INDIVIDUAL} or {@link #ROW_OFFSET_MODE_PAGE}.
+ */
+ public void setRowOffsetMode(@RowOffsetMode int mode) {
+ if (mode == mRowOffsetMode) {
+ return;
+ }
+
+ mRowOffsetMode = mode;
+ offsetRows();
+ }
+
+ /**
+ * Sets the listener that will be notified of various scroll events in the list.
+ *
+ * @param listener The on-scroll listener.
+ */
+ public void setOnScrollListener(PagedListView.OnScrollListener listener) {
+ mOnScrollListener = listener;
+ }
+
+ /**
+ * Finish the pagination taking into account where the gesture started (not where we are now).
+ *
+ * @return Whether the list was scrolled as a result of the fling.
+ */
+ public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) {
+ if (getChildCount() == 0) {
+ return false;
+ }
+
+ if (mReachedLimitOfDrag) {
+ return false;
+ }
+
+ // If the fling was too slow or too short, settle on the first fully visible row instead.
+ if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE
+ || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) {
+ int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+ if (firstFullyVisibleChildIndex != -1) {
+ int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex));
+ parent.smoothScrollToPosition(scrollPosition);
+ return true;
+ }
+ return false;
+ }
+
+ // Finish the pagination taking into account where the gesture
+ // started (not where we are now).
+ boolean isDownGesture = flingVelocity > 0 || (flingVelocity == 0 && mLastDragDistance >= 0);
+ boolean isUpGesture = flingVelocity < 0 || (flingVelocity == 0 && mLastDragDistance < 0);
+ if (isDownGesture && mLowerPageBreakPosition != -1) {
+ // If the last view is fully visible then only settle on the first fully visible view
+ // instead of the original page down position. However, don't page down if the last
+ // item has come fully into view.
+ parent.smoothScrollToPosition(mAnchorPageBreakPosition);
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onGestureDown();
+ }
+ return true;
+ } else if (isUpGesture && mUpperPageBreakPosition != -1) {
+ parent.smoothScrollToPosition(mUpperPageBreakPosition);
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onGestureUp();
+ }
+ return true;
+ } else {
+ Log.e(
+ TAG,
+ "Error setting scroll for fling! flingVelocity: \t"
+ + flingVelocity
+ + "\tlastDragDistance: "
+ + mLastDragDistance
+ + "\tpageUpAtStartOfDrag: "
+ + mUpperPageBreakPosition
+ + "\tpageDownAtStartOfDrag: "
+ + mLowerPageBreakPosition);
+ // As a last resort, at the last smooth scroller target position if there is one.
+ if (mSmoothScroller != null) {
+ parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @return The position that paging up from the current position would settle at. */
+ public int getPageUpPosition() {
+ return mUpperPageBreakPosition;
+ }
+
+ /** @return The position that paging down from the current position would settle at. */
+ public int getPageDownPosition() {
+ return mLowerPageBreakPosition;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ SavedState savedState = new SavedState();
+ savedState.mFirstChildPosition = getFirstFullyVisibleChildPosition();
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ scrollToPosition(((SavedState) state).mFirstChildPosition);
+ }
+ }
+
+ /** The state that will be saved across configuration changes. */
+ static class SavedState implements Parcelable {
+ /** The position of the first visible child view in the list. */
+ int mFirstChildPosition;
+
+ SavedState() {}
+
+ private SavedState(Parcel in) {
+ mFirstChildPosition = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mFirstChildPosition);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Layout the anchor row. The anchor row is the first fully visible row.
+ *
+ * @param anchorTop The decorated top of the anchor. If it is not known or should be reset to
+ * the top, pass -1.
+ */
+ private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) {
+ View anchor = recycler.getViewForPosition(anchorPosition);
+ RecyclerView.LayoutParams params = getParams(anchor);
+ measureChildWithMargins(anchor, 0, 0);
+ int left = getPaddingLeft() + params.leftMargin;
+ int top = (anchorTop == -1) ? params.topMargin : anchorTop;
+ int right = left + getDecoratedMeasuredWidth(anchor);
+ int bottom = top + getDecoratedMeasuredHeight(anchor);
+ layoutDecorated(anchor, left, top, right, bottom);
+ addView(anchor);
+ return anchor;
+ }
+
+ /**
+ * Lays out the next row in the specified direction next to the specified adjacent row.
+ *
+ * @param recycler The recycler from which a new view can be created.
+ * @param adjacentRow The View of the adjacent row which will be used to position the new one.
+ * @param layoutDirection The side of the adjacent row that the new row will be laid out on.
+ * @return The new row that was laid out.
+ */
+ private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow,
+ @LayoutDirection int layoutDirection) {
+ int adjacentRowPosition = getPosition(adjacentRow);
+ int newRowPosition = adjacentRowPosition;
+ if (layoutDirection == BEFORE) {
+ newRowPosition = adjacentRowPosition - 1;
+ } else if (layoutDirection == AFTER) {
+ newRowPosition = adjacentRowPosition + 1;
+ }
+
+ // Because we detach all rows in onLayoutChildren, this will often just return a view from
+ // the scrap heap.
+ View newRow = recycler.getViewForPosition(newRowPosition);
+
+ measureChildWithMargins(newRow, 0, 0);
+ RecyclerView.LayoutParams newRowParams =
+ (RecyclerView.LayoutParams) newRow.getLayoutParams();
+ RecyclerView.LayoutParams adjacentRowParams =
+ (RecyclerView.LayoutParams) adjacentRow.getLayoutParams();
+ int left = getPaddingLeft() + newRowParams.leftMargin;
+ int right = left + getDecoratedMeasuredWidth(newRow);
+ int top;
+ int bottom;
+ if (layoutDirection == BEFORE) {
+ bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin;
+ top = bottom - getDecoratedMeasuredHeight(newRow);
+ } else {
+ top = getDecoratedBottom(adjacentRow) + adjacentRowParams.bottomMargin
+ + newRowParams.topMargin;
+ bottom = top + getDecoratedMeasuredHeight(newRow);
+ }
+ layoutDecorated(newRow, left, top, right, bottom);
+
+ if (layoutDirection == BEFORE) {
+ addView(newRow, 0);
+ } else {
+ addView(newRow);
+ }
+
+ return newRow;
+ }
+
+ /** @return Whether another row should be laid out in the specified direction. */
+ private boolean shouldLayoutNextRow(
+ RecyclerView.State state, View adjacentRow, @LayoutDirection int layoutDirection) {
+ int adjacentRowPosition = getPosition(adjacentRow);
+
+ if (layoutDirection == BEFORE) {
+ if (adjacentRowPosition == 0) {
+ // We already laid out the first row.
+ return false;
+ }
+ } else if (layoutDirection == AFTER) {
+ if (adjacentRowPosition >= state.getItemCount() - 1) {
+ // We already laid out the last row.
+ return false;
+ }
+ }
+
+ // If we are scrolling layout views until the target position.
+ if (mSmoothScroller != null) {
+ if (layoutDirection == BEFORE
+ && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) {
+ return true;
+ } else if (layoutDirection == AFTER
+ && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) {
+ return true;
+ }
+ }
+
+ View focusedRow = getFocusedChild();
+ if (focusedRow != null) {
+ int focusedRowPosition = getPosition(focusedRow);
+ if (layoutDirection == BEFORE && adjacentRowPosition
+ >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
+ return true;
+ } else if (layoutDirection == AFTER && adjacentRowPosition
+ <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
+ return true;
+ }
+ }
+
+ RecyclerView.LayoutParams params = getParams(adjacentRow);
+ int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin;
+ int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin;
+ if (layoutDirection == BEFORE && adjacentRowTop < getPaddingTop() - getHeight()) {
+ // View is more than 1 page past the top of the screen and also past where the user has
+ // scrolled to. We want to keep one page past the top to make the scroll up calculation
+ // easier and scrolling smoother.
+ return false;
+ } else if (layoutDirection == AFTER
+ && adjacentRowBottom > getHeight() - getPaddingBottom()) {
+ // View is off of the bottom and also past where the user has scrolled to.
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Remove and recycle views that are no longer needed. */
+ private void recycleChildrenFromStart(RecyclerView.Recycler recycler) {
+ // Start laying out children one page before the top of the viewport.
+ int childrenStart = getPaddingTop() - getHeight();
+
+ int focusedChildPosition = Integer.MAX_VALUE;
+ View focusedChild = getFocusedChild();
+ if (focusedChild != null) {
+ focusedChildPosition = getPosition(focusedChild);
+ }
+
+ // Count the number of views that should be removed.
+ int detachedCount = 0;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ int childEnd = getDecoratedBottom(child);
+ int childPosition = getPosition(child);
+
+ if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) {
+ break;
+ }
+
+ detachedCount++;
+ }
+
+ // Remove the number of views counted above. Done by removing the first child n times.
+ while (--detachedCount >= 0) {
+ final View child = getChildAt(0);
+ removeAndRecycleView(child, recycler);
+ }
+ }
+
+ /** Remove and recycle views that are no longer needed. */
+ private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) {
+ // Layout views until the end of the viewport.
+ int childrenEnd = getHeight();
+
+ int focusedChildPosition = Integer.MIN_VALUE + 1;
+ View focusedChild = getFocusedChild();
+ if (focusedChild != null) {
+ focusedChildPosition = getPosition(focusedChild);
+ }
+
+ // Count the number of views that should be removed.
+ int firstDetachedPos = 0;
+ int detachedCount = 0;
+ int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ int childStart = getDecoratedTop(child);
+ int childPosition = getPosition(child);
+
+ if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) {
+ break;
+ }
+
+ firstDetachedPos = i;
+ detachedCount++;
+ }
+
+ while (--detachedCount >= 0) {
+ final View child = getChildAt(firstDetachedPos);
+ removeAndRecycleView(child, recycler);
+ }
+ }
+
+ /**
+ * Offset rows to do fancy animations. If offset rows was not enabled with
+ * {@link #setOffsetRows}, this will do nothing.
+ *
+ * @see #offsetRowsIndividually
+ * @see #offsetRowsByPage
+ * @see #setOffsetRows
+ */
+ public void offsetRows() {
+ if (!mOffsetRows) {
+ return;
+ }
+
+ if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) {
+ offsetRowsByPage();
+ } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) {
+ offsetRowsIndividually();
+ }
+ }
+
+ /**
+ * Offset the single row that is scrolling off the screen such that by the time the next row
+ * reaches the top, it will have accelerated completely off of the screen.
+ */
+ private void offsetRowsIndividually() {
+ if (getChildCount() == 0) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, ":: offsetRowsIndividually getChildCount=0");
+ }
+ return;
+ }
+
+ // Identify the dangling row. It will be the first row that is at the top of the
+ // list or above.
+ int danglingChildIndex = -1;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) {
+ danglingChildIndex = i;
+ break;
+ }
+ }
+
+ mAnchorPageBreakPosition = danglingChildIndex;
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex);
+ }
+
+ // Calculate the total amount that the view will need to scroll in order to go completely
+ // off screen.
+ RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
+ int[] locs = new int[2];
+ rv.getLocationInWindow(locs);
+ int listTopInWindow = locs[1] + rv.getPaddingTop();
+ int maxDanglingViewTranslation;
+
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ RecyclerView.LayoutParams params = getParams(child);
+
+ maxDanglingViewTranslation = listTopInWindow;
+ // If the child has a negative margin, we'll actually need to translate the view a
+ // little but further to get it completely off screen.
+ if (params.topMargin < 0) {
+ maxDanglingViewTranslation -= params.topMargin;
+ }
+ if (params.bottomMargin < 0) {
+ maxDanglingViewTranslation -= params.bottomMargin;
+ }
+
+ if (i < danglingChildIndex) {
+ child.setAlpha(0f);
+ } else if (i > danglingChildIndex) {
+ child.setAlpha(1f);
+ setCardFlyingEffectOffset(child, 0);
+ } else {
+ int totalScrollDistance =
+ getDecoratedMeasuredHeight(child) + params.topMargin + params.bottomMargin;
+
+ int distanceLeftInScroll =
+ getDecoratedBottom(child) + params.bottomMargin - getPaddingTop();
+ float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance;
+ float interpolatedPercentage =
+ mDanglingRowInterpolator.getInterpolation(percentageIntoScroll);
+
+ child.setAlpha(1f);
+ setCardFlyingEffectOffset(child, -(maxDanglingViewTranslation
+ * interpolatedPercentage));
+ }
+ }
+ }
+
+ /**
+ * When the list scrolls, the entire page of rows will offset in one contiguous block. This
+ * significantly reduces the amount of extra motion at the top of the screen.
+ */
+ private void offsetRowsByPage() {
+ View anchorView = findViewByPosition(mAnchorPageBreakPosition);
+ if (anchorView == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, ":: offsetRowsByPage anchorView null");
+ }
+ return;
+ }
+ int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin;
+
+ View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
+ int upperViewTop =
+ getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
+
+ int scrollDistance = upperViewTop - anchorViewTop;
+
+ int distanceLeft = anchorViewTop - getPaddingTop();
+ float scrollPercentage =
+ (Math.abs(scrollDistance) - distanceLeft) / (float) Math.abs(scrollDistance);
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format(":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, "
+ + "scrollPercentage:%s",
+ scrollDistance, distanceLeft, scrollPercentage));
+ }
+
+ // Calculate the total amount that the view will need to scroll in order to go completely
+ // off screen.
+ RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
+ int[] locs = new int[2];
+ rv.getLocationInWindow(locs);
+ int listTopInWindow = locs[1] + rv.getPaddingTop();
+
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ int position = getPosition(child);
+ if (position < mUpperPageBreakPosition) {
+ child.setAlpha(0f);
+ setCardFlyingEffectOffset(child, -listTopInWindow);
+ } else if (position < mAnchorPageBreakPosition) {
+ // If the child has a negative margin, we need to offset the row by a little bit
+ // extra so that it moves completely off screen.
+ RecyclerView.LayoutParams params = getParams(child);
+ int extraTranslation = 0;
+ if (params.topMargin < 0) {
+ extraTranslation -= params.topMargin;
+ }
+ if (params.bottomMargin < 0) {
+ extraTranslation -= params.bottomMargin;
+ }
+ int translation = (int) ((listTopInWindow + extraTranslation)
+ * mDanglingRowInterpolator.getInterpolation(scrollPercentage));
+ child.setAlpha(1f);
+ setCardFlyingEffectOffset(child, -translation);
+ } else {
+ child.setAlpha(1f);
+ setCardFlyingEffectOffset(child, 0);
+ }
+ }
+ }
+
+ /**
+ * Apply an offset to this view. This offset is applied post-layout so it doesn't affect when
+ * views are recycled
+ *
+ * @param child The view to apply this to
+ * @param verticalOffset The offset for this child.
+ */
+ private void setCardFlyingEffectOffset(View child, float verticalOffset) {
+ // Ideally instead of doing all this, we could use View.setTranslationY(). However, the
+ // default RecyclerView.ItemAnimator also uses this method which causes layout issues.
+ // See: http://b/25977087
+ TranslateAnimation anim = mFlyOffscreenAnimations.get(child);
+ if (anim == null) {
+ anim = new TranslateAnimation();
+ anim.setFillEnabled(true);
+ anim.setFillAfter(true);
+ anim.setDuration(0);
+ mFlyOffscreenAnimations.put(child, anim);
+ } else if (anim.verticalOffset == verticalOffset) {
+ return;
+ }
+
+ anim.reset();
+ anim.verticalOffset = verticalOffset;
+ anim.setStartTime(Animation.START_ON_FIRST_FRAME);
+ child.setAnimation(anim);
+ anim.startNow();
+ }
+
+ /**
+ * Update the page break positions based on the position of the views on screen. This should be
+ * called whenever view move or change such as during a scroll or layout.
+ */
+ private void updatePageBreakPositions() {
+ if (getChildCount() == 0) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0");
+ }
+ return;
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions "
+ + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, "
+ + "mLowerPageBreakPosition:%s",
+ mAnchorPageBreakPosition, mUpperPageBreakPosition,
+ mLowerPageBreakPosition));
+ }
+
+ mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild());
+
+ if (mAnchorPageBreakPosition == -1) {
+ Log.w(TAG, "Unable to update anchor positions. There is no anchor position.");
+ return;
+ }
+
+ View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition);
+ if (anchorPageBreakView == null) {
+ return;
+ }
+ int topMargin = getParams(anchorPageBreakView).topMargin;
+ int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin;
+ View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
+ int upperPageBreakTop = upperPageBreakView == null
+ ? Integer.MIN_VALUE
+ : getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s"
+ + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
+ + " mLowerPageBreakPosition:%s",
+ topMargin,
+ anchorTop,
+ mAnchorPageBreakPosition,
+ mUpperPageBreakPosition,
+ mLowerPageBreakPosition));
+ }
+
+ if (anchorTop < getPaddingTop()) {
+ // The anchor has moved above the viewport. We are now on the next page. Shift the page
+ // break positions and calculate a new lower one.
+ mUpperPageBreakPosition = mAnchorPageBreakPosition;
+ mAnchorPageBreakPosition = mLowerPageBreakPosition;
+ mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
+ } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) {
+ // The anchor has moved below the viewport. We are now on the previous page. Shift
+ // the page break positions and calculate a new upper one.
+ mLowerPageBreakPosition = mAnchorPageBreakPosition;
+ mAnchorPageBreakPosition = mUpperPageBreakPosition;
+ mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
+ } else {
+ mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
+ mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions"
+ + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
+ + " mLowerPageBreakPosition:%s",
+ mAnchorPageBreakPosition, mUpperPageBreakPosition,
+ mLowerPageBreakPosition));
+ }
+ }
+
+ /**
+ * @return The page break position of the page before the anchor page break position. However,
+ * if it reaches the end of the laid out children or position 0, it will just return that.
+ */
+ @VisibleForTesting
+ int calculatePreviousPageBreakPosition(int position) {
+ if (position == -1) {
+ return -1;
+ }
+ View referenceView = findViewByPosition(position);
+ int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
+
+ int previousPagePosition = position;
+ while (previousPagePosition > 0) {
+ previousPagePosition--;
+ View child = findViewByPosition(previousPagePosition);
+ if (child == null) {
+ // View has not been laid out yet.
+ return previousPagePosition + 1;
+ }
+
+ int childTop = getDecoratedTop(child) - getParams(child).topMargin;
+ if (childTop < referenceViewTop - getHeight()) {
+ return previousPagePosition + 1;
+ }
+ }
+ // Beginning of the list.
+ return 0;
+ }
+
+ /**
+ * @return The page break position of the next page after the anchor page break position.
+ * However, if it reaches the end of the laid out children or end of the list, it will just
+ * return that.
+ */
+ @VisibleForTesting
+ int calculateNextPageBreakPosition(int position) {
+ if (position == -1) {
+ return -1;
+ }
+
+ View referenceView = findViewByPosition(position);
+ if (referenceView == null) {
+ return position;
+ }
+ int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
+
+ int nextPagePosition = position;
+
+ // Search for the first child item after the referenceView that didn't fully fit on to the
+ // screen. The next page should start from the item before this child, so that users have
+ // a visual anchoring point of the page change.
+ while (nextPagePosition < getItemCount() - 1) {
+ nextPagePosition++;
+ View child = findViewByPosition(nextPagePosition);
+ if (child == null) {
+ // The next view has not been laid out yet.
+ return nextPagePosition - 1;
+ }
+
+ int childTop = getDecoratedTop(child) - getParams(child).topMargin;
+ if (childTop > referenceViewTop + getHeight()) {
+ // If choosing the previous child causes the view to snap back to the referenceView
+ // position, then skip that and go directly to the child. This avoids the case
+ // where a tall card in the layout causes the view to constantly snap back to
+ // the top when scrolled.
+ return nextPagePosition - 1 == position ? nextPagePosition : nextPagePosition - 1;
+ }
+ }
+ // End of the list.
+ return nextPagePosition;
+ }
+
+ /**
+ * In this style, the focus will scroll down to the middle of the screen and lock there so that
+ * moving in either direction will move the entire list by 1.
+ */
+ private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) {
+ int focusedPosition = getPosition(child);
+ if (focusedPosition == mLastChildPositionToRequestFocus) {
+ return true;
+ }
+ mLastChildPositionToRequestFocus = focusedPosition;
+
+ int availableHeight = getAvailableHeight();
+ int focusedChildTop = getDecoratedTop(child);
+ int focusedChildBottom = getDecoratedBottom(child);
+
+ int childIndex = parent.indexOfChild(child);
+ // Iterate through children starting at the focused child to find the child above it to
+ // smooth scroll to such that the focused child will be as close to the middle of the screen
+ // as possible.
+ for (int i = childIndex; i >= 0; i--) {
+ View childAtI = getChildAt(i);
+ if (childAtI == null) {
+ Log.e(TAG, "Child is null at index " + i);
+ continue;
+ }
+ // We haven't found a view that is more than half of the recycler view height above it
+ // but we've reached the top so we can't go any further.
+ if (i == 0) {
+ parent.smoothScrollToPosition(getPosition(childAtI));
+ break;
+ }
+
+ // Because we want to scroll to the first view that is less than half of the screen
+ // away from the focused view, we "look ahead" one view. When the look ahead view
+ // is more than availableHeight / 2 away, the current child at i is the one we want to
+ // scroll to. However, sometimes, that view can be null (ie, if the view is in
+ // transition). In that case, just skip that view.
+
+ View childBefore = getChildAt(i - 1);
+ if (childBefore == null) {
+ continue;
+ }
+ int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore);
+ int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore);
+
+ if (distanceToChildBeforeFromTop > availableHeight / 2
+ || distanceToChildBeforeFromBottom > availableHeight) {
+ parent.smoothScrollToPosition(getPosition(childAtI));
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * We don't actually know the size of every single view, only what is currently laid out. This
+ * makes it difficult to do accurate scrollbar calculations. However, lists in the car often
+ * consist of views with identical heights. Because of that, we can use a single sample view to
+ * do our calculations for. The main exceptions are in the first items of a list (hero card,
+ * last call card, etc) so if the first view is at position 0, we pick the next one.
+ *
+ * @return The decorated measured height of the sample view plus its margins.
+ */
+ private int getSampleViewHeight() {
+ if (mSampleViewHeight != -1) {
+ return mSampleViewHeight;
+ }
+ int sampleViewIndex = getFirstFullyVisibleChildIndex();
+ View sampleView = getChildAt(sampleViewIndex);
+ if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) {
+ sampleView = getChildAt(++sampleViewIndex);
+ }
+ RecyclerView.LayoutParams params = getParams(sampleView);
+ int height = getDecoratedMeasuredHeight(sampleView) + params.topMargin
+ + params.bottomMargin;
+ if (height == 0) {
+ // This can happen if the view isn't measured yet.
+ Log.w(
+ TAG,
+ "The sample view has a height of 0. Returning a dummy value for now "
+ + "that won't be cached.");
+ height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height);
+ } else {
+ mSampleViewHeight = height;
+ }
+ return height;
+ }
+
+ /** @return The height of the RecyclerView excluding padding. */
+ private int getAvailableHeight() {
+ return getHeight() - getPaddingTop() - getPaddingBottom();
+ }
+
+ /**
+ * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child of
+ * {@link RecyclerView}.
+ */
+ private static RecyclerView.LayoutParams getParams(View view) {
+ return (RecyclerView.LayoutParams) view.getLayoutParams();
+ }
+
+ /**
+ * Custom {@link LinearSmoothScroller} that has: a) Custom control over the speed of scrolls. b)
+ * Scrolling snaps to start. All of our scrolling logic depends on that. c) Keeps track of some
+ * state of the current scroll so that can aid in things like the scrollbar calculations.
+ */
+ private final class CarSmoothScroller extends LinearSmoothScroller {
+ /** This value (150) was hand tuned by UX for what felt right. * */
+ private static final float MILLISECONDS_PER_INCH = 150f;
+ /** This value (0.45) was hand tuned by UX for what felt right. * */
+ private static final float DECELERATION_TIME_DIVISOR = 0.45f;
+
+ /** This value (1.8) was hand tuned by UX for what felt right. * */
+ private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f);
+
+ private final int mTargetPosition;
+
+ CarSmoothScroller(Context context, int targetPosition) {
+ super(context);
+ mTargetPosition = targetPosition;
+ }
+
+ @Override
+ public PointF computeScrollVectorForPosition(int i) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex()));
+ final int direction = (mTargetPosition < firstChildPos) ? -1 : 1;
+ return new PointF(0, direction);
+ }
+
+ @Override
+ protected int getVerticalSnapPreference() {
+ // This is key for most of the scrolling logic that guarantees that scrolling
+ // will settle with a view aligned to the top.
+ return LinearSmoothScroller.SNAP_TO_START;
+ }
+
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+ if (dy == 0) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Scroll distance is 0");
+ }
+ return;
+ }
+
+ final int time = calculateTimeForDeceleration(dy);
+ if (time > 0) {
+ action.update(0, -dy, time, mInterpolator);
+ }
+ }
+
+ @Override
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+ }
+
+ @Override
+ protected int calculateTimeForDeceleration(int dx) {
+ return (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR);
+ }
+
+ @Override
+ public int getTargetPosition() {
+ return mTargetPosition;
+ }
+ }
+
+ /**
+ * Animation that translates a view by the specified amount. Used for card flying off the screen
+ * effect.
+ */
+ private static class TranslateAnimation extends Animation {
+ public float verticalOffset;
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ super.applyTransformation(interpolatedTime, t);
+ t.getMatrix().setTranslate(0, verticalOffset);
+ }
+ }
+}
diff --git a/car/src/main/java/android/support/car/widget/PagedListView.java b/car/src/main/java/android/support/car/widget/PagedListView.java
new file mode 100644
index 0000000..67a6247
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedListView.java
@@ -0,0 +1,996 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.UiThread;
+import android.support.car.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that
+ * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the
+ * left side.
+ */
+public class PagedListView extends FrameLayout {
+ /** Default maximum number of clicks allowed on a list */
+ public static final int DEFAULT_MAX_CLICKS = 6;
+
+ /**
+ * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the
+ * maximum number of pages to show.
+ */
+ public static final int UNLIMITED_PAGES = -1;
+
+ /**
+ * The amount of time after settling to wait before autoscrolling to the next page when the user
+ * holds down a pagination button.
+ */
+ protected static final int PAGINATION_HOLD_DELAY_MS = 400;
+
+ private static final String TAG = "PagedListView";
+ private static final int INVALID_RESOURCE_ID = -1;
+
+ protected final CarRecyclerView mRecyclerView;
+ protected final PagedLayoutManager mLayoutManager;
+ protected final Handler mHandler = new Handler();
+ private final boolean mScrollBarEnabled;
+ private final PagedScrollBarView mScrollBarView;
+
+ private int mRowsPerPage = -1;
+ protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
+
+ /** Maximum number of pages to show. */
+ private int mMaxPages;
+
+ protected OnScrollListener mOnScrollListener;
+
+ /** Number of visible rows per page */
+ private int mDefaultMaxPages = DEFAULT_MAX_CLICKS;
+
+ /** Used to check if there are more items added to the list. */
+ private int mLastItemCount = 0;
+
+ private boolean mNeedsFocus;
+
+ /**
+ * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of
+ * items.
+ *
+ * <p>NOTE: it is still up to the adapter to use maxItems in {@link
+ * android.support.v7.widget.RecyclerView.Adapter#getItemCount()}.
+ *
+ * <p>the recommended way would be with:
+ *
+ * <pre>{@code
+ * {@literal@}Override
+ * public int getItemCount() {
+ * return Math.min(super.getItemCount(), mMaxItems);
+ * }
+ * }</pre>
+ */
+ public interface ItemCap {
+ /**
+ * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
+ */
+ int UNLIMITED = -1;
+
+ /**
+ * Sets the maximum number of items available in the adapter. A value less than '0' means
+ * the list should not be capped.
+ */
+ void setMaxItems(int maxItems);
+ }
+
+ /**
+ * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position
+ * offset for the adapter to load the data.
+ *
+ * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show
+ * the item in position 20 instead, for position 1 it will show the item in position 21 instead
+ * and so on.
+ */
+ public interface ItemPositionOffset {
+ /** Sets the position offset for the adapter. */
+ void setPositionOffset(int positionOffset);
+ }
+
+ public PagedListView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
+ }
+
+ public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) {
+ this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
+ }
+
+ public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ this(context, attrs, defStyleAttrs, defStyleRes, 0);
+ }
+
+ public PagedListView(
+ Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) {
+ super(context, attrs, defStyleAttrs, defStyleRes);
+ if (layoutId == 0) {
+ layoutId = R.layout.car_paged_recycler_view;
+ }
+ LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes);
+ mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view);
+ boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false);
+ mRecyclerView.setFadeLastItem(fadeLastItem);
+ boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false);
+
+ mMaxPages = getDefaultMaxPages();
+
+ mLayoutManager = new PagedLayoutManager(context);
+ mLayoutManager.setOffsetRows(offsetRows);
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
+ mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
+ mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager));
+
+ boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
+ if (offsetScrollBar) {
+ MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
+ params.setMarginStart(getResources().getDimensionPixelSize(
+ R.dimen.car_margin));
+ params.setMarginEnd(
+ a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0));
+ mRecyclerView.setLayoutParams(params);
+ }
+
+ if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
+ int dividerStartMargin = a.getDimensionPixelSize(
+ R.styleable.PagedListView_dividerStartMargin, 0);
+ int dividerStartId = a.getResourceId(
+ R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID);
+ int dividerEndId = a.getResourceId(
+ R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID);
+
+ mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin,
+ dividerStartId, dividerEndId));
+ }
+
+ int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0);
+ if (itemSpacing > 0) {
+ mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
+ }
+
+ // Set this to true so that this view consumes clicks events and views underneath
+ // don't receive this click event. Without this it's possible to click places in the
+ // view that don't capture the event, and as a result, elements visually hidden consume
+ // the event.
+ setClickable(true);
+
+ // Set focusable false explicitly to handle the behavior change in Android O where
+ // clickable view becomes focusable by default.
+ setFocusable(false);
+
+ mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true);
+ mScrollBarView = (PagedScrollBarView) findViewById(R.id.paged_scroll_view);
+ mScrollBarView.setPaginationListener(
+ new PagedScrollBarView.PaginationListener() {
+ @Override
+ public void onPaginate(int direction) {
+ if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) {
+ mRecyclerView.pageUp();
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollUpButtonClicked();
+ }
+ } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) {
+ mRecyclerView.pageDown();
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollDownButtonClicked();
+ }
+ } else {
+ Log.e(TAG, "Unknown pagination direction (" + direction + ")");
+ }
+ }
+ });
+
+ Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
+ if (upButtonIcon != null) {
+ setUpButtonIcon(upButtonIcon);
+ }
+
+ Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon);
+ if (downButtonIcon != null) {
+ setDownButtonIcon(downButtonIcon);
+ }
+
+ mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);
+
+ // Modify the layout the Scroll Bar is not visible.
+ if (!mScrollBarEnabled) {
+ MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
+ params.setMarginStart(0);
+ mRecyclerView.setLayoutParams(params);
+ }
+
+ setDayNightStyle(DayNightStyle.AUTO);
+ a.recycle();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mHandler.removeCallbacks(mUpdatePaginationRunnable);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ if (e.getAction() == MotionEvent.ACTION_DOWN) {
+ // The user has interacted with the list using touch. All movements will now paginate
+ // the list.
+ mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_PAGE);
+ }
+ return super.onInterceptTouchEvent(e);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ // The user has interacted with the list using the controller. Movements through the list
+ // will now be one row at a time.
+ mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL);
+ }
+
+ /**
+ * Returns the position of the given View in the list.
+ *
+ * @param v The View to check for.
+ * @return The position or -1 if the given View is {@code null} or not in the list.
+ */
+ public int positionOf(@Nullable View v) {
+ if (v == null || v.getParent() != mRecyclerView) {
+ return -1;
+ }
+ return mLayoutManager.getPosition(v);
+ }
+
+ @NonNull
+ public CarRecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ /**
+ * Scrolls to the given position in the PagedListView.
+ *
+ * @param position The position in the list to scroll to.
+ */
+ public void scrollToPosition(int position) {
+ mLayoutManager.scrollToPosition(position);
+
+ // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
+ // the pagination arrows actually get updated. See b/http://b/15801119
+ mHandler.post(mUpdatePaginationRunnable);
+ }
+
+ /** Sets the icon to be used for the up button. */
+ public void setUpButtonIcon(Drawable icon) {
+ mScrollBarView.setUpButtonIcon(icon);
+ }
+
+ /** Sets the icon to be used for the down button. */
+ public void setDownButtonIcon(Drawable icon) {
+ mScrollBarView.setDownButtonIcon(icon);
+ }
+
+ /**
+ * Sets the adapter for the list.
+ *
+ * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of
+ * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as
+ * {@link #setMaxPages(int)}, will do nothing.
+ */
+ public void setAdapter(
+ @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
+ mAdapter = adapter;
+ mRecyclerView.setAdapter(adapter);
+ updateMaxItems();
+ }
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @NonNull
+ public PagedLayoutManager getLayoutManager() {
+ return mLayoutManager;
+ }
+
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
+ return mRecyclerView.getAdapter();
+ }
+
+ /**
+ * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a
+ * page is defined as the number of items that fit completely on the screen at once.
+ *
+ * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number
+ * of pages.
+ *
+ * <p>Note that for any restriction on maximum pages to work, the adapter passed to this
+ * PagedListView needs to implement {@link ItemCap}.
+ *
+ * @param maxPages The maximum number of pages that fit on the screen. Should be positive or
+ * {@link #UNLIMITED_PAGES}.
+ */
+ public void setMaxPages(int maxPages) {
+ mMaxPages = Math.max(UNLIMITED_PAGES, maxPages);
+ updateMaxItems();
+ }
+
+ /**
+ * Returns the maximum number of pages allowed in the PagedListView. This number is set by
+ * {@link #setMaxPages(int)}. If that method has not been called, then this value should match
+ * the default value.
+ *
+ * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is
+ * no limit.
+ */
+ public int getMaxPages() {
+ return mMaxPages;
+ }
+
+ /**
+ * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
+ * PagedLayoutManager is null or the height of the first child is 0, it will return 1.
+ */
+ public int getRowsPerPage() {
+ return mRowsPerPage;
+ }
+
+ /** Resets the maximum number of pages to be shown to be the default. */
+ public void resetMaxPages() {
+ mMaxPages = getDefaultMaxPages();
+ updateMaxItems();
+ }
+
+ /**
+ * @return The position of first visible child in the list. -1 will be returned if there is no
+ * child.
+ */
+ public int getFirstFullyVisibleChildPosition() {
+ return mLayoutManager.getFirstFullyVisibleChildPosition();
+ }
+
+ /**
+ * @return The position of last visible child in the list. -1 will be returned if there is no
+ * child.
+ */
+ public int getLastFullyVisibleChildPosition() {
+ return mLayoutManager.getLastFullyVisibleChildPosition();
+ }
+
+ /**
+ * Adds an {@link android.support.v7.widget.RecyclerView.ItemDecoration} to this PagedListView.
+ *
+ * @param decor The decoration to add.
+ * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration)
+ */
+ public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
+ mRecyclerView.addItemDecoration(decor);
+ }
+
+ /**
+ * Removes the given {@link android.support.v7.widget.RecyclerView.ItemDecoration} from this
+ * PagedListView.
+ *
+ * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}.
+ *
+ * @param decor The decoration to remove.
+ * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration)
+ */
+ public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
+ mRecyclerView.removeItemDecoration(decor);
+ }
+
+ /**
+ * Sets spacing between each item in the list. The spacing will not be added before the first
+ * item and after the last.
+ *
+ * @param itemSpacing the spacing between each item.
+ */
+ public void setItemSpacing(int itemSpacing) {
+ ItemSpacingDecoration existing = null;
+ for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
+ RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
+ if (itemDecoration instanceof ItemSpacingDecoration) {
+ existing = (ItemSpacingDecoration) itemDecoration;
+ break;
+ }
+ }
+
+ if (itemSpacing == 0 && existing != null) {
+ mRecyclerView.removeItemDecoration(existing);
+ } else if (existing == null) {
+ mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
+ } else {
+ existing.setItemSpacing(itemSpacing);
+ }
+ mRecyclerView.invalidateItemDecorations();
+ }
+
+ /**
+ * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this
+ * PagedListView.
+ *
+ * <p>The listener will function the same as the listener for a regular {@link RecyclerView}.
+ *
+ * @param touchListener The touch listener to add.
+ * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)
+ */
+ public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
+ mRecyclerView.addOnItemTouchListener(touchListener);
+ }
+
+ /**
+ * Removes the given {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} from
+ * the PagedListView.
+ *
+ * @param touchListener The touch listener to remove.
+ * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener)
+ */
+ public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
+ mRecyclerView.removeOnItemTouchListener(touchListener);
+ }
+ /**
+ * Sets how this {@link PagedListView} responds to day/night configuration changes. By
+ * default, the PagedListView is darker in the day and lighter at night.
+ *
+ * @param dayNightStyle A value from {@link DayNightStyle}.
+ * @see DayNightStyle
+ */
+ public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
+ // Update the scrollbar
+ mScrollBarView.setDayNightStyle(dayNightStyle);
+
+ int decorCount = mRecyclerView.getItemDecorationCount();
+ for (int i = 0; i < decorCount; i++) {
+ RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
+ if (decor instanceof DividerDecoration) {
+ ((DividerDecoration) decor).updateDividerColor();
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the
+ * last child in the PagedListView that is fully visible.
+ *
+ * @return The corresponding ViewHolder or {@code null} if none exists.
+ */
+ @Nullable
+ public RecyclerView.ViewHolder getLastViewHolder() {
+ View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild();
+ if (lastFullyVisible == null) {
+ return null;
+ }
+ int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible);
+ RecyclerView.ViewHolder lastViewHolder = getRecyclerView()
+ .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1);
+ // We want to get the very last ViewHolder in the list, even if it's only fully visible
+ // If it doesn't exist, return the last fully visible ViewHolder.
+ if (lastViewHolder == null) {
+ lastViewHolder = getRecyclerView()
+ .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition);
+ }
+ return lastViewHolder;
+ }
+
+ /**
+ * Sets the {@link OnScrollListener} that will be notified of scroll events within the
+ * PagedListView.
+ *
+ * @param listener The scroll listener to set.
+ */
+ public void setOnScrollListener(OnScrollListener listener) {
+ mOnScrollListener = listener;
+ mLayoutManager.setOnScrollListener(mOnScrollListener);
+ }
+
+ /** Returns the page the given position is on, starting with page 0. */
+ public int getPage(int position) {
+ if (mRowsPerPage == -1) {
+ return -1;
+ }
+ if (mRowsPerPage == 0) {
+ return 0;
+ }
+ return position / mRowsPerPage;
+ }
+
+ /**
+ * Sets the default number of pages that this PagedListView is limited to.
+ *
+ * @param newDefault The default number of pages. Should be positive.
+ */
+ public void setDefaultMaxPages(int newDefault) {
+ if (newDefault < 0) {
+ return;
+ }
+ mDefaultMaxPages = newDefault;
+ resetMaxPages();
+ }
+
+ /** Returns the default number of pages the list should have */
+ protected int getDefaultMaxPages() {
+ // assume list shown in response to a click, so, reduce number of clicks by one
+ return mDefaultMaxPages - 1;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // if a late item is added to the top of the layout after the layout is stabilized, causing
+ // the former top item to be pushed to the 2nd page, the focus will still be on the former
+ // top item. Since our car layout manager tries to scroll the viewport so that the focused
+ // item is visible, the view port will be on the 2nd page. That means the newly added item
+ // will not be visible, on the first page.
+
+ // what we want to do is: if the formerly focused item is the first one in the list, any
+ // item added above it will make the focus to move to the new first item.
+ // if the focus is not on the formerly first item, then we don't need to do anything. Let
+ // the layout manager do the job and scroll the viewport so the currently focused item
+ // is visible.
+
+ // we need to calculate whether we want to request focus here, before the super call,
+ // because after the super call, the first born might be changed.
+ View focusedChild = mLayoutManager.getFocusedChild();
+ View firstBorn = mLayoutManager.getChildAt(0);
+
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mAdapter != null) {
+ int itemCount = mAdapter.getItemCount();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, String.format(
+ "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, "
+ + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, "
+ + "mNeedsFocus: %s",
+ hasFocus(),
+ mLastItemCount,
+ itemCount,
+ focusedChild,
+ firstBorn,
+ isInTouchMode(),
+ mNeedsFocus));
+ }
+ updateMaxItems();
+ // This is a workaround for missing focus because isInTouchMode() is not always
+ // returning the right value.
+ // This is okay for the Engine release since focus is always showing.
+ // However, in Tala and Fender, we want to show focus only when the user uses
+ // hardware controllers, so we need to revisit this logic. b/22990605.
+ if (mNeedsFocus && itemCount > 0) {
+ if (focusedChild == null) {
+ requestFocus();
+ }
+ mNeedsFocus = false;
+ }
+ if (itemCount > mLastItemCount && focusedChild == firstBorn) {
+ requestFocus();
+ }
+ mLastItemCount = itemCount;
+ }
+ // We need to update the scroll buttons after layout has happened.
+ // Determining if a scrollbar is necessary requires looking at the layout of the child
+ // views. Therefore, this determination can only be done after layout has happened.
+ // Note: don't animate here to prevent b/26849677
+ updatePaginationButtons(false /*animate*/);
+ }
+
+ /**
+ * Returns the View at the given position within the list.
+ *
+ * @param position A position within the list.
+ * @return The View or {@code null} if no View exists at the given position.
+ */
+ @Nullable
+ public View findViewByPosition(int position) {
+ return mLayoutManager.findViewByPosition(position);
+ }
+
+ /**
+ * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
+ * being called as a result of adapter changes, it should be called after the new layout has
+ * been calculated because the method of determining scrollbar visibility uses the current
+ * layout. If this is called after an adapter change but before the new layout, the visibility
+ * determination may not be correct.
+ *
+ * @param animate {@code true} if the scrollbar should animate to its new position.
+ * {@code false} if no animation is used
+ */
+ protected void updatePaginationButtons(boolean animate) {
+ if (!mScrollBarEnabled) {
+ // Don't change the visibility of the ScrollBar unless it's enabled.
+ return;
+ }
+
+ if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom())
+ || mLayoutManager.getItemCount() == 0) {
+ mScrollBarView.setVisibility(View.INVISIBLE);
+ } else {
+ mScrollBarView.setVisibility(View.VISIBLE);
+ }
+ mScrollBarView.setUpEnabled(shouldEnablePageUpButton());
+ mScrollBarView.setDownEnabled(shouldEnablePageDownButton());
+
+ mScrollBarView.setParameters(
+ mRecyclerView.computeVerticalScrollRange(),
+ mRecyclerView.computeVerticalScrollOffset(),
+ mRecyclerView.computeVerticalScrollExtent(),
+ animate);
+ invalidate();
+ }
+
+ protected boolean shouldEnablePageUpButton() {
+ return !mLayoutManager.isAtTop();
+ }
+
+ protected boolean shouldEnablePageDownButton() {
+ return !mLayoutManager.isAtBottom();
+ }
+
+ @UiThread
+ protected void updateMaxItems() {
+ if (mAdapter == null) {
+ return;
+ }
+
+ // Ensure mRowsPerPage regardless of if the adapter implements ItemCap.
+ updateRowsPerPage();
+
+ // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
+ if (!(mAdapter instanceof ItemCap)) {
+ return;
+ }
+
+ final int originalCount = mAdapter.getItemCount();
+ ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount());
+ final int newCount = mAdapter.getItemCount();
+ if (newCount == originalCount) {
+ return;
+ }
+
+ if (newCount < originalCount) {
+ mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
+ } else {
+ mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
+ }
+ }
+
+ protected int calculateMaxItemCount() {
+ final View firstChild = mLayoutManager.getChildAt(0);
+ if (firstChild == null || firstChild.getHeight() == 0) {
+ return -1;
+ } else {
+ return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages;
+ }
+ }
+
+ /**
+ * Updates the rows number per current page, which is used for calculating how many items we
+ * want to show.
+ */
+ protected void updateRowsPerPage() {
+ final View firstChild = mLayoutManager.getChildAt(0);
+ if (firstChild == null || firstChild.getHeight() == 0) {
+ mRowsPerPage = 1;
+ } else {
+ mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight());
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ SavedState savedState = new SavedState(super.onSaveInstanceState());
+ savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState();
+ return savedState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState);
+ super.onRestoreInstanceState(savedState.getSuperState());
+ }
+
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ // There is the possibility of multiple PagedListViews on a page. This means that the ids
+ // of the child Views of PagedListView are no longer unique, and onSaveInstanceState()
+ // cannot be used. As a result, PagedListViews needs to manually dispatch the instance
+ // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState()
+ // called by the system.
+ dispatchFreezeSelfOnly(container);
+ }
+
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView
+ // will manually handle passing the state. See the comment in dispatchSaveInstanceState()
+ // for more information.
+ dispatchThawSelfOnly(container);
+ }
+
+ /** The state that will be saved across configuration changes. */
+ private static class SavedState extends BaseSavedState {
+ /** The state of the {@link #mLayoutManager} of this PagedListView. */
+ Parcelable mLayoutManagerState;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ mLayoutManagerState =
+ in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeParcelable(mLayoutManagerState, flags);
+ }
+
+ public static final ClassLoaderCreator<SavedState> CREATOR =
+ new ClassLoaderCreator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel source, ClassLoader loader) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState createFromParcel(Parcel source) {
+ return createFromParcel(source, null /* loader */);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrolled(recyclerView, dx, dy);
+ if (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) {
+ mOnScrollListener.onReachBottom();
+ } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) {
+ mOnScrollListener.onLeaveBottom();
+ }
+ }
+ updatePaginationButtons(false);
+ }
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(recyclerView, newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
+ }
+ }
+ };
+
+ protected final Runnable mPaginationRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ boolean upPressed = mScrollBarView.isUpPressed();
+ boolean downPressed = mScrollBarView.isDownPressed();
+ if (upPressed && downPressed) {
+ return;
+ }
+ if (upPressed) {
+ mRecyclerView.pageUp();
+ } else if (downPressed) {
+ mRecyclerView.pageDown();
+ }
+ }
+ };
+
+ private final Runnable mUpdatePaginationRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ updatePaginationButtons(true /*animate*/);
+ }
+ };
+
+ /** Used to listen for {@code PagedListView} scroll events. */
+ public abstract static class OnScrollListener {
+ /** Called when menu reaches the bottom */
+ public void onReachBottom() {}
+ /** Called when menu leaves the bottom */
+ public void onLeaveBottom() {}
+ /** Called when scroll up button is clicked */
+ public void onScrollUpButtonClicked() {}
+ /** Called when scroll down button is clicked */
+ public void onScrollDownButtonClicked() {}
+ /** Called when scrolling to the previous page via up gesture */
+ public void onGestureUp() {}
+ /** Called when scrolling to the next page via down gesture */
+ public void onGestureDown() {}
+
+ /**
+ * Called when RecyclerView.OnScrollListener#onScrolled is called. See
+ * RecyclerView.OnScrollListener
+ */
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
+
+ /** See RecyclerView.OnScrollListener */
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
+
+ /** Called when the view scrolls up a page */
+ public void onPageUp() {}
+
+ /** Called when the view scrolls down a page */
+ public void onPageDown() {}
+ }
+
+ /**
+ * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will add spacing
+ * between each item in the RecyclerView that it is added to.
+ */
+ private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
+
+ private int mHalfItemSpacing;
+
+ private ItemSpacingDecoration(int itemSpacing) {
+ mHalfItemSpacing = itemSpacing / 2;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ // Skip top offset for first item and bottom offset for last.
+ int position = parent.getChildAdapterPosition(view);
+ if (position > 0) {
+ outRect.top = mHalfItemSpacing;
+ }
+ if (position < state.getItemCount() - 1) {
+ outRect.bottom = mHalfItemSpacing;
+ }
+ }
+
+ /**
+ * @param itemSpacing sets spacing between each item.
+ */
+ public void setItemSpacing(int itemSpacing) {
+ mHalfItemSpacing = itemSpacing / 2;
+ }
+ }
+
+ /**
+ * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing
+ * line between each item in the RecyclerView that it is added to.
+ */
+ private static class DividerDecoration extends RecyclerView.ItemDecoration {
+ private final Context mContext;
+ private final Paint mPaint;
+ private final int mDividerHeight;
+ private final int mDividerStartMargin;
+ @IdRes private final int mDividerStartId;
+ @IdRes private final int mDividerEndId;
+
+ /**
+ * @param dividerStartMargin The start offset of the dividing line. This offset will be
+ * relative to {@code dividerStartId} if that value is given.
+ * @param dividerStartId A child view id whose starting edge will be used as the starting
+ * edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
+ * container of each child view will be used.
+ * @param dividerEndId A child view id whose ending edge will be used as the starting edge
+ * of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top
+ * container view of each child will be used.
+ */
+ private DividerDecoration(Context context, int dividerStartMargin,
+ @IdRes int dividerStartId, @IdRes int dividerEndId) {
+ mContext = context;
+ mDividerStartMargin = dividerStartMargin;
+ mDividerStartId = dividerStartId;
+ mDividerEndId = dividerEndId;
+
+ Resources res = context.getResources();
+ mPaint = new Paint();
+ mPaint.setColor(res.getColor(R.color.car_list_divider));
+ mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height);
+ }
+
+ /** Updates the list divider color which may have changed due to a day night transition. */
+ public void updateDividerColor() {
+ mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider));
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ // Draw a divider line between each item. No need to draw the line for the last item.
+ for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) {
+ View container = parent.getChildAt(i);
+ View nextContainer = parent.getChildAt(i + 1);
+ int spacing = nextContainer.getTop() - container.getBottom();
+
+ View startChild =
+ mDividerStartId != INVALID_RESOURCE_ID
+ ? container.findViewById(mDividerStartId)
+ : container;
+
+ View endChild =
+ mDividerEndId != INVALID_RESOURCE_ID
+ ? container.findViewById(mDividerEndId)
+ : container;
+
+ if (startChild == null || endChild == null) {
+ continue;
+ }
+
+ int left = mDividerStartMargin + startChild.getLeft();
+ int right = endChild.getRight();
+ int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2;
+ int top = bottom - mDividerHeight;
+
+ c.drawRect(left, top, right, bottom, mPaint);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ // Skip top offset for first item and bottom offset for last.
+ int position = parent.getChildAdapterPosition(view);
+ if (position > 0) {
+ outRect.top = mDividerHeight / 2;
+ }
+ if (position < state.getItemCount() - 1) {
+ outRect.bottom = mDividerHeight / 2;
+ }
+ }
+ }
+}
diff --git a/car/src/main/java/android/support/car/widget/PagedScrollBarView.java b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
new file mode 100644
index 0000000..1c46b5d
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.car.R;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */
+public class PagedScrollBarView extends FrameLayout
+ implements View.OnClickListener, View.OnLongClickListener {
+ private static final float BUTTON_DISABLED_ALPHA = 0.2f;
+
+ @DayNightStyle private int mDayNightStyle;
+
+ /** Listener for when the list should paginate. */
+ public interface PaginationListener {
+ int PAGE_UP = 0;
+ int PAGE_DOWN = 1;
+
+ /** Called when the linked view should be paged in the given direction */
+ void onPaginate(int direction);
+ }
+
+ private final ImageView mUpButton;
+ private final ImageView mDownButton;
+ private final ImageView mScrollThumb;
+ /** The "filler" view between the up and down buttons */
+ private final View mFiller;
+
+ private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
+ private final int mMinThumbLength;
+ private final int mMaxThumbLength;
+ private PaginationListener mPaginationListener;
+
+ public PagedScrollBarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
+ }
+
+ public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) {
+ this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
+ }
+
+ public PagedScrollBarView(
+ Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ super(context, attrs, defStyleAttrs, defStyleRes);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.car_paged_scrollbar_buttons, this /* root */,
+ true /* attachToRoot */);
+
+ mUpButton = (ImageView) findViewById(R.id.page_up);
+ mUpButton.setOnClickListener(this);
+ mUpButton.setOnLongClickListener(this);
+ mDownButton = (ImageView) findViewById(R.id.page_down);
+ mDownButton.setOnClickListener(this);
+ mDownButton.setOnLongClickListener(this);
+
+ mScrollThumb = (ImageView) findViewById(R.id.scrollbar_thumb);
+ mFiller = findViewById(R.id.filler);
+
+ mMinThumbLength = getResources().getDimensionPixelSize(R.dimen.min_thumb_height);
+ mMaxThumbLength = getResources().getDimensionPixelSize(R.dimen.max_thumb_height);
+ }
+
+ @Override
+ public void onClick(View v) {
+ dispatchPageClick(v);
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ dispatchPageClick(v);
+ return true;
+ }
+
+ /** Sets the icon to be used for the up button. */
+ public void setUpButtonIcon(Drawable icon) {
+ mUpButton.setImageDrawable(icon);
+ }
+
+ /** Sets the icon to be used for the down button. */
+ public void setDownButtonIcon(Drawable icon) {
+ mDownButton.setImageDrawable(icon);
+ }
+
+ /**
+ * Sets the listener that will be notified when the up and down buttons have been pressed.
+ *
+ * @param listener The listener to set.
+ */
+ public void setPaginationListener(PaginationListener listener) {
+ mPaginationListener = listener;
+ }
+
+ /** Returns {@code true} if the "up" button is pressed */
+ public boolean isUpPressed() {
+ return mUpButton.isPressed();
+ }
+
+ /** Returns {@code true} if the "down" button is pressed */
+ public boolean isDownPressed() {
+ return mDownButton.isPressed();
+ }
+
+ /** Sets the range, offset and extent of the scroll bar. See {@link View}. */
+ public void setParameters(int range, int offset, int extent, boolean animate) {
+ // This method is where we take the computed parameters from the PagedLayoutManager and
+ // render it within the specified constraints ({@link #mMaxThumbLength} and
+ // {@link #mMinThumbLength}).
+ final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom();
+
+ int thumbLength = extent * size / range;
+ thumbLength = Math.max(Math.min(thumbLength, mMaxThumbLength), mMinThumbLength);
+
+ int thumbOffset = size - thumbLength;
+ if (isDownEnabled()) {
+ // We need to adjust the offset so that it fits into the possible space inside the
+ // filler with regarding to the constraints set by mMaxThumbLength and mMinThumbLength.
+ thumbOffset = (size - thumbLength) * offset / range;
+ }
+
+ // Sets the size of the thumb and request a redraw if needed.
+ final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+ if (lp.height != thumbLength) {
+ lp.height = thumbLength;
+ mScrollThumb.requestLayout();
+ }
+
+ moveY(mScrollThumb, thumbOffset, animate);
+ }
+
+ /**
+ * Sets how this {@link PagedScrollBarView} responds to day/night configuration changes. By
+ * default, the PagedScrollBarView is darker in the day and lighter at night.
+ *
+ * @param dayNightStyle A value from {@link DayNightStyle}.
+ * @see DayNightStyle
+ */
+ public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
+ mDayNightStyle = dayNightStyle;
+ reloadColors();
+ }
+
+ /**
+ * Sets whether or not the up button on the scroll bar is clickable.
+ *
+ * @param enabled {@code true} if the up button is enabled.
+ */
+ public void setUpEnabled(boolean enabled) {
+ mUpButton.setEnabled(enabled);
+ mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
+ }
+
+ /**
+ * Sets whether or not the down button on the scroll bar is clickable.
+ *
+ * @param enabled {@code true} if the down button is enabled.
+ */
+ public void setDownEnabled(boolean enabled) {
+ mDownButton.setEnabled(enabled);
+ mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
+ }
+
+ /**
+ * Returns whether or not the down button on the scroll bar is clickable.
+ *
+ * @return {@code true} if the down button is enabled. {@code false} otherwise.
+ */
+ public boolean isDownEnabled() {
+ return mDownButton.isEnabled();
+ }
+
+ /** Reload the colors for the current {@link DayNightStyle}. */
+ private void reloadColors() {
+ int tint;
+ int thumbBackground;
+ int upDownBackgroundResId;
+
+ switch (mDayNightStyle) {
+ case DayNightStyle.AUTO:
+ tint = ContextCompat.getColor(getContext(), R.color.car_tint);
+ thumbBackground = ContextCompat.getColor(getContext(),
+ R.color.car_scrollbar_thumb);
+ upDownBackgroundResId = R.drawable.car_pagination_background;
+ break;
+ case DayNightStyle.AUTO_INVERSE:
+ tint = ContextCompat.getColor(getContext(), R.color.car_tint_inverse);
+ thumbBackground = ContextCompat.getColor(getContext(),
+ R.color.car_scrollbar_thumb_inverse);
+ upDownBackgroundResId = R.drawable.car_pagination_background_inverse;
+ break;
+ case DayNightStyle.FORCE_NIGHT:
+ tint = ContextCompat.getColor(getContext(), R.color.car_tint_light);
+ thumbBackground = ContextCompat.getColor(getContext(),
+ R.color.car_scrollbar_thumb_light);
+ upDownBackgroundResId = R.drawable.car_pagination_background_night;
+ break;
+ case DayNightStyle.FORCE_DAY:
+ tint = ContextCompat.getColor(getContext(), R.color.car_tint_dark);
+ thumbBackground = ContextCompat.getColor(getContext(),
+ R.color.car_scrollbar_thumb_dark);
+ upDownBackgroundResId = R.drawable.car_pagination_background_day;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle);
+ }
+
+ mScrollThumb.setBackgroundColor(thumbBackground);
+
+ mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
+ mUpButton.setBackgroundResource(upDownBackgroundResId);
+
+ mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
+ mDownButton.setBackgroundResource(upDownBackgroundResId);
+ }
+
+ private void dispatchPageClick(View v) {
+ final PaginationListener listener = mPaginationListener;
+ if (listener == null) {
+ return;
+ }
+
+ int direction = v.getId() == R.id.page_up
+ ? PaginationListener.PAGE_UP
+ : PaginationListener.PAGE_DOWN;
+ listener.onPaginate(direction);
+ }
+
+ /** Moves the given view to the specified 'y' position. */
+ private void moveY(final View view, float newPosition, boolean animate) {
+ final int duration = animate ? 200 : 0;
+ view.animate()
+ .y(newPosition)
+ .setDuration(duration)
+ .setInterpolator(mPaginationInterpolator)
+ .start();
+ }
+}
diff --git a/car/tests/AndroidManifest.xml b/car/tests/AndroidManifest.xml
new file mode 100644
index 0000000..949e85a
--- /dev/null
+++ b/car/tests/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.car.widget.test">
+ <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+ <application android:supportsRtl="true">
+ <activity android:name="android.support.car.widget.ColumnCardViewTestActivity"/>
+ <activity android:name="android.support.car.widget.PagedListViewSavedStateActivity"/>
+ <activity android:name="android.support.car.widget.PagedListViewTestActivity"/>
+ </application>
+</manifest>
diff --git a/car/tests/NO_DOCS b/car/tests/NO_DOCS
new file mode 100644
index 0000000..bd77b1a
--- /dev/null
+++ b/car/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
\ No newline at end of file
diff --git a/car/tests/res/drawable/ic_thumb_down.xml b/car/tests/res/drawable/ic_thumb_down.xml
new file mode 100644
index 0000000..25fccdb
--- /dev/null
+++ b/car/tests/res/drawable/ic_thumb_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M30,6L12,6c-1.66,0 -3.08,1.01 -3.68,2.44l-6.03,14.1C2.11,23 2,23.49 2,24v3.83l0.02,0.02L2,28c0,2.21 1.79,4 4,4h12.63l-1.91,9.14c-0.04,0.2 -0.07,0.41 -0.07,0.63 0,0.83 0.34,1.58 0.88,2.12L19.66,46l13.17,-13.17C33.55,32.1 34,31.1 34,30L34,10c0,-2.21 -1.79,-4 -4,-4zM38,6v24h8L46,6h-8z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/car/tests/res/drawable/ic_thumb_up.xml b/car/tests/res/drawable/ic_thumb_up.xml
new file mode 100644
index 0000000..9f02cf3
--- /dev/null
+++ b/car/tests/res/drawable/ic_thumb_up.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M2,42h8L10,18L2,18v24zM46,20c0,-2.21 -1.79,-4 -4,-4L29.37,16l1.91,-9.14c0.04,-0.2 0.07,-0.41 0.07,-0.63 0,-0.83 -0.34,-1.58 -0.88,-2.12L28.34,2 15.17,15.17C14.45,15.9 14,16.9 14,18v20c0,2.21 1.79,4 4,4h18c1.66,0 3.08,-1.01 3.68,-2.44l6.03,-14.1c0.18,-0.46 0.29,-0.95 0.29,-1.46v-3.83l-0.02,-0.02L46,20z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/car/tests/res/layout/activity_column_card_view.xml b/car/tests/res/layout/activity_column_card_view.xml
new file mode 100644
index 0000000..ad9c5e1
--- /dev/null
+++ b/car/tests/res/layout/activity_column_card_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:car="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.car.widget.ColumnCardView
+ android:id="@+id/default_width_column_card"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <android.support.car.widget.ColumnCardView
+ car:columnSpan="2"
+ android:id="@+id/span_2_column_card"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/car/tests/res/layout/activity_paged_list_view.xml b/car/tests/res/layout/activity_paged_list_view.xml
new file mode 100644
index 0000000..d14eb96
--- /dev/null
+++ b/car/tests/res/layout/activity_paged_list_view.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/frame_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.car.widget.PagedListView
+ android:id="@+id/paged_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:showPagedListViewDivider="false"
+ app:offsetScrollBar="true"/>
+</FrameLayout>
diff --git a/car/tests/res/layout/activity_two_paged_list_view.xml b/car/tests/res/layout/activity_two_paged_list_view.xml
new file mode 100644
index 0000000..588071f
--- /dev/null
+++ b/car/tests/res/layout/activity_two_paged_list_view.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <android.support.car.widget.PagedListView
+ android:id="@+id/paged_list_view_1"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:showPagedListViewDivider="false"
+ app:offsetScrollBar="true"/>
+
+ <android.support.car.widget.PagedListView
+ android:id="@+id/paged_list_view_2"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:showPagedListViewDivider="false"
+ app:offsetScrollBar="true"/>
+
+</LinearLayout>
diff --git a/car/tests/res/layout/paged_list_item_column_card.xml b/car/tests/res/layout/paged_list_item_column_card.xml
new file mode 100644
index 0000000..2bc5cd0
--- /dev/null
+++ b/car/tests/res/layout/paged_list_item_column_card.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <android.support.car.widget.ColumnCardView
+ android:id="@+id/column_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </android.support.car.widget.ColumnCardView>
+
+</FrameLayout>
diff --git a/car/tests/src/android/support/car/widget/ColumnCardViewTest.java b/car/tests/src/android/support/car/widget/ColumnCardViewTest.java
new file mode 100644
index 0000000..cb61caf
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/ColumnCardViewTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.support.car.test.R;
+import android.support.car.utils.ColumnCalculator;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.ViewTreeObserver;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Instrumentation unit tests for {@link ColumnCardView}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ColumnCardViewTest {
+ @Rule
+ public ActivityTestRule<ColumnCardViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(ColumnCardViewTestActivity.class);
+
+ private ColumnCalculator mCalculator;
+ private ColumnCardViewTestActivity mActivity;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mCalculator = ColumnCalculator.getInstance(mActivity);
+ }
+
+ @Test
+ public void defaultCardWidthMatchesCalculation() {
+ ColumnCardView card = mActivity.findViewById(R.id.default_width_column_card);
+
+ assertEquals(mCalculator.getSizeForColumnSpan(mActivity.getResources().getInteger(
+ R.integer.column_card_default_column_span)),
+ card.getWidth());
+ }
+
+ @Test
+ public void customXmlColumnSpanMatchesCalculation() {
+ ColumnCardView card = mActivity.findViewById(R.id.span_2_column_card);
+
+ assertEquals(mCalculator.getSizeForColumnSpan(2), card.getWidth());
+ }
+
+ @UiThreadTest
+ @Test
+ public void settingColumnSpanMatchesCalculation() {
+ final int columnSpan = 4;
+ final ColumnCardView card = mActivity.findViewById(R.id.span_2_column_card);
+ assertNotEquals(columnSpan, card.getColumnSpan());
+
+ card.setColumnSpan(columnSpan);
+ // When card finishes layout, verify its updated width.
+ card.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ assertEquals(mCalculator.getSizeForColumnSpan(columnSpan), card.getWidth());
+ }
+ });
+ }
+
+ @UiThreadTest
+ @Test
+ public void nonPositiveColumnSpanIsIgnored() {
+ final ColumnCardView card = mActivity.findViewById(R.id.default_width_column_card);
+ final int original = card.getColumnSpan();
+
+ card.setColumnSpan(0);
+ // When card finishes layout, verify its width remains unchanged.
+ card.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ assertEquals(mCalculator.getSizeForColumnSpan(original), card.getWidth());
+ }
+ });
+ }
+}
diff --git a/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
new file mode 100644
index 0000000..693e4a1
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.car.test.R;
+
+public class ColumnCardViewTestActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_column_card_view);
+ }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java b/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java
new file mode 100644
index 0000000..8cb976c
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.car.test.R;
+
+/**
+ * Test Activity for testing the saving of state for the {@link PagedListView}. It will inflate
+ * a layout that has two PagedListViews next to each other.
+ */
+public class PagedListViewSavedStateActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_two_paged_list_view);
+ }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
new file mode 100644
index 0000000..9b871b3
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertEquals;
+
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.support.car.test.R;
+import android.support.test.espresso.IdlingRegistry;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/** Unit tests for the ability of the {@link PagedListView} to save state. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class PagedListViewSavedStateTest {
+ /**
+ * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
+ * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
+ * to ITEMS_PER_PAGE * desired_pages.
+ *
+ * <p>Actual value does not matter.
+ */
+ private static final int ITEMS_PER_PAGE = 5;
+
+ /**
+ * The total number of items to display in a list. This value just needs to be large enough
+ * to ensure the scroll bar shows.
+ */
+ private static final int TOTAL_ITEMS_IN_LIST = 100;
+
+ private static final int NUM_OF_PAGES = TOTAL_ITEMS_IN_LIST / ITEMS_PER_PAGE;
+
+ @Rule
+ public ActivityTestRule<PagedListViewSavedStateActivity> mActivityRule =
+ new ActivityTestRule<>(PagedListViewSavedStateActivity.class);
+
+ private PagedListViewSavedStateActivity mActivity;
+ private PagedListView mPagedListView1;
+ private PagedListView mPagedListView2;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+
+ mPagedListView1 = mActivity.findViewById(R.id.paged_list_view_1);
+ mPagedListView2 = mActivity.findViewById(R.id.paged_list_view_2);
+
+ setUpPagedListView(mPagedListView1);
+ setUpPagedListView(mPagedListView2);
+ }
+
+ private boolean isAutoDevice() {
+ PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ private void setUpPagedListView(PagedListView pagedListView) {
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ pagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+ pagedListView.setAdapter(new TestAdapter(TOTAL_ITEMS_IN_LIST,
+ pagedListView.getMeasuredHeight()));
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ @After
+ public void tearDown() {
+ for (IdlingResource idlingResource : IdlingRegistry.getInstance().getResources()) {
+ IdlingRegistry.getInstance().unregister(idlingResource);
+ }
+ }
+
+ @Suppress
+ @Test
+ public void testPagePositionRememberedOnRotation() {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ Random random = new Random();
+ IdlingRegistry.getInstance().register(new PagedListViewScrollingIdlingResource(
+ mPagedListView1, mPagedListView2));
+
+ // Add 1 to this random number to ensure it is a value between 1 and NUM_OF_PAGES.
+ int numOfClicks = random.nextInt(NUM_OF_PAGES) + 1;
+ clickPageDownButton(onPagedListView1(), numOfClicks);
+ int topPositionOfPagedListView1 =
+ mPagedListView1.getLayoutManager().getFirstFullyVisibleChildPosition();
+
+ numOfClicks = random.nextInt(NUM_OF_PAGES) + 1;
+ clickPageDownButton(onPagedListView2(), numOfClicks);
+ int topPositionOfPagedListView2 =
+ mPagedListView2.getLayoutManager().getFirstFullyVisibleChildPosition();
+
+ // Perform a configuration change by rotating the screen.
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+
+ // Check that the positions are the same after the change.
+ assertEquals(topPositionOfPagedListView1,
+ mPagedListView1.getLayoutManager().getFirstFullyVisibleChildPosition());
+ assertEquals(topPositionOfPagedListView2,
+ mPagedListView2.getLayoutManager().getFirstFullyVisibleChildPosition());
+ }
+
+ /** Clicks the page down button on the given PagedListView for the given number of times. */
+ private void clickPageDownButton(Matcher<View> pagedListView, int times) {
+ for (int i = 0; i < times; i++) {
+ onView(allOf(withId(R.id.page_down), pagedListView)).perform(click());
+ }
+ }
+
+
+ /** Convenience method for checking that a View is on the first PagedListView. */
+ private Matcher<View> onPagedListView1() {
+ return isDescendantOfA(withId(R.id.paged_list_view_1));
+ }
+
+ /** Convenience method for checking that a View is on the second PagedListView. */
+ private Matcher<View> onPagedListView2() {
+ return isDescendantOfA(withId(R.id.paged_list_view_2));
+ }
+
+ private static String getItemText(int index) {
+ return "Data " + index;
+ }
+
+ /** An Adapter that ensures that there is {@link #ITEMS_PER_PAGE} displayed. */
+ private class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
+ implements PagedListView.ItemCap {
+ private List<String> mData;
+ private int mParentHeight;
+
+ TestAdapter(int itemCount, int parentHeight) {
+ mData = new ArrayList<>();
+ for (int i = 0; i < itemCount; i++) {
+ mData.add(getItemText(i));
+ }
+ mParentHeight = parentHeight;
+ }
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ return new TestViewHolder(inflater, parent);
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder, int position) {
+ // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
+ int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
+ holder.itemView.setMinimumHeight(height);
+ holder.setText(mData.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+
+ @Override
+ public void setMaxItems(int maxItems) {
+ // No-op
+ }
+ }
+
+ /** A ViewHolder that holds a View with a TextView. */
+ private class TestViewHolder extends RecyclerView.ViewHolder {
+ private TextView mTextView;
+
+ TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
+ super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
+ mTextView = itemView.findViewById(R.id.text_view);
+ }
+
+ public void setText(String text) {
+ mTextView.setText(text);
+ }
+ }
+
+ // Registering IdlingResource in @Before method does not work - espresso doesn't actually wait
+ // for ViewAction to finish. So each method that clicks on button will need to register their
+ // own IdlingResource.
+ private class PagedListViewScrollingIdlingResource implements IdlingResource {
+ private boolean mIsIdle = true;
+ private ResourceCallback mResourceCallback;
+
+ PagedListViewScrollingIdlingResource(PagedListView pagedListView1,
+ PagedListView pagedListView2) {
+ // Ensure the IdlingResource waits for both RecyclerViews to finish their movement.
+ pagedListView1.getRecyclerView().addOnScrollListener(mOnScrollListener);
+ pagedListView2.getRecyclerView().addOnScrollListener(mOnScrollListener);
+ }
+
+ @Override
+ public String getName() {
+ return PagedListViewScrollingIdlingResource.class.getName();
+ }
+
+ @Override
+ public boolean isIdleNow() {
+ return mIsIdle;
+ }
+
+ @Override
+ public void registerIdleTransitionCallback(ResourceCallback callback) {
+ mResourceCallback = callback;
+ }
+
+ private final RecyclerView.OnScrollListener mOnScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(
+ RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+
+ // Treat dragging as idle, or Espresso will block itself when
+ // swiping.
+ mIsIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
+ || newState == RecyclerView.SCROLL_STATE_DRAGGING);
+
+ if (mIsIdle && mResourceCallback != null) {
+ mResourceCallback.onTransitionToIdle();
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
+ };
+ }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTest.java b/car/tests/src/android/support/car/widget/PagedListViewTest.java
new file mode 100644
index 0000000..0d07fbd
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewTest.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.action.ViewActions.swipeDown;
+import static android.support.test.espresso.action.ViewActions.swipeUp;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
+import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.support.car.test.R;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.Espresso;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link PagedListView}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class PagedListViewTest {
+
+ /**
+ * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
+ * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
+ * to ITEMS_PER_PAGE * desired_pages.
+ * Actual value does not matter.
+ */
+ private static final int ITEMS_PER_PAGE = 5;
+
+ @Rule
+ public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(PagedListViewTestActivity.class);
+
+ private PagedListViewTestActivity mActivity;
+ private PagedListView mPagedListView;
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityRule.getActivity();
+ mPagedListView = mActivity.findViewById(R.id.paged_list_view);
+
+ // Using deprecated Espresso methods instead of calling it on the IdlingRegistry because
+ // the latter does not seem to work as reliably. Specifically, on the latter, it does
+ // not always register and unregister.
+ Espresso.registerIdlingResources(new PagedListViewScrollingIdlingResource(mPagedListView));
+ }
+
+ @After
+ public void tearDown() {
+ for (IdlingResource idlingResource : Espresso.getIdlingResources()) {
+ Espresso.unregisterIdlingResources(idlingResource);
+ }
+ }
+
+ private boolean isAutoDevice() {
+ PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ private void setUpPagedListView(int itemCount) {
+ setUpPagedListView(itemCount, PagedListView.ItemCap.UNLIMITED);
+ }
+
+ private void setUpPagedListView(int itemCount, int maxPages) {
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setMaxPages(maxPages);
+ mPagedListView.setAdapter(
+ new TestAdapter(itemCount, mPagedListView.getMeasuredHeight()));
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ /** Initializes {@link #mPagedListView} with an adapter that does not implement ItemCap. */
+ public void setUpNonItemCapPagedListView(int itemCount, int maxPages) {
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setMaxPages(maxPages);
+ mPagedListView.setAdapter(
+ new NoItemCapAdapter(itemCount, mPagedListView.getMeasuredHeight()));
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ @Test
+ public void scrollBarIsInvisibleIfItemsDoNotFillOnePage() {
+ setUpPagedListView(1 /* itemCount */);
+
+ onView(withId(R.id.paged_scroll_view)).check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void pageUpDownButtonIsDisabledOnListEnds() throws Throwable {
+ final int itemCount = ITEMS_PER_PAGE * 3;
+ setUpPagedListView(itemCount);
+ // Initially page_up button is disabled.
+ onView(withId(R.id.page_up)).check(matches(not(isEnabled())));
+
+ // Moving to middle of list enables page_up button.
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount / 2));
+ onView(withId(R.id.page_up)).check(matches(isEnabled()));
+
+ // Moving to page end, page_down button is disabled.
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount));
+ onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+ }
+
+ @Test
+ public void testMaxPageGetterSetterDefaultValue() {
+ final int maxPages = 2;
+ final int defaultMaxPages = 3;
+
+ // setMaxPages
+ setUpPagedListView(ITEMS_PER_PAGE, maxPages);
+ assertThat(mPagedListView.getMaxPages(), is(equalTo(maxPages)));
+
+ // resetMaxPages
+ mPagedListView.resetMaxPages();
+ // Max pages is equal to max clicks - 1
+ assertThat(mPagedListView.getMaxPages(), is(equalTo(PagedListView.DEFAULT_MAX_CLICKS - 1)));
+
+ // setDefaultMaxPages
+ mPagedListView.setDefaultMaxPages(defaultMaxPages);
+ mPagedListView.resetMaxPages();
+ assertThat(mPagedListView.getMaxPages(), is(equalTo(defaultMaxPages - 1)));
+ }
+
+ @Test
+ public void setMaxPagesLimitsNumberOfClicks() {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ setUpPagedListView(ITEMS_PER_PAGE * 3 /* itemCount */, 2 /* maxPages */);
+
+ onView(withId(R.id.page_down)).perform(click());
+ onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+ }
+
+ @Test
+ public void testMaxPagesDoesNothingIfAdapterDoesNotImplementItemCap() {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ int numOfPages = 20;
+ int maxPages = 2;
+
+ setUpNonItemCapPagedListView(ITEMS_PER_PAGE * numOfPages, maxPages);
+
+ // There should be no limit on the scroll even though a max number of pages was set.
+ for (int i = 0; i < maxPages; i++) {
+ onView(withId(R.id.page_down)).perform(click());
+ }
+ onView(withId(R.id.page_down)).check(matches(isEnabled()));
+
+ // Next scroll all the way to bottom and check this is possible.
+ for (int i = 0; i < numOfPages - maxPages; i++) {
+ onView(withId(R.id.page_down)).perform(click());
+ }
+ onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+ }
+
+ @Suppress
+ @Test
+ public void resetMaxPagesToDefaultUnlimitedExtendsList() throws Throwable {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ final int itemCount = ITEMS_PER_PAGE * 4;
+ setUpPagedListView(itemCount, 2 /* maxPages */);
+
+ // Move to next page - should reach end of list.
+ onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
+
+ // After resetting max pages (default unlimited), we scroll to the known total number of
+ // items.
+ mActivityRule.runOnUiThread(() -> mPagedListView.resetMaxPages());
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount - 1));
+
+ // Verify the last item that would've been hidden due to max pages is now shown.
+ onView(allOf(withId(R.id.text_view), withText(itemText(itemCount - 1))))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void scrollbarKeepsItemSnappedToTopOfList() {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ // 2.5 so last page is not full
+ setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
+
+ // Going down one page and first item is snapped to top
+ onView(withId(R.id.page_down)).perform(click());
+ verifyItemSnappedToListTop();
+
+ // Go down another page and we reach the last page.
+ onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
+ verifyItemSnappedToListTop();
+ }
+
+ @Suppress
+ @Test
+ public void swipeUpKeepsItemSnappedToTopOfList() {
+ setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
+
+ onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(1, swipeUp()));
+
+ verifyItemSnappedToListTop();
+ }
+
+ @Suppress
+ @Test
+ public void swipeDownKeepsItemSnappedToTopOfList() throws Throwable {
+ setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
+
+ // Go down one page, then swipe down (going up).
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(ITEMS_PER_PAGE));
+ onView(withId(R.id.recycler_view))
+ .perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeDown()));
+
+ verifyItemSnappedToListTop();
+ }
+
+ @Test
+ public void pageUpAndDownMoveSameDistance() {
+ if (!isAutoDevice()) {
+ return;
+ }
+
+ setUpPagedListView(ITEMS_PER_PAGE * 10);
+
+ // Move down one page so there will be sufficient pages for up and downs.
+ onView(withId(R.id.page_down)).perform(click());
+ final int topPosition = mPagedListView.getFirstFullyVisibleChildPosition();
+
+ for (int i = 0; i < 3; i++) {
+ onView(withId(R.id.page_down)).perform(click());
+ onView(withId(R.id.page_up)).perform(click());
+ }
+
+ assertThat(mPagedListView.getFirstFullyVisibleChildPosition(), is(equalTo(topPosition)));
+ }
+
+ @Suppress
+ @Test
+ public void setItemSpacing() throws Throwable {
+ final int itemCount = 3;
+ setUpPagedListView(itemCount /* itemCount */);
+
+ // Initial spacing is 0.
+ final View[] views = new View[itemCount];
+ mActivityRule.runOnUiThread(() -> {
+ for (int i = 0; i < itemCount; i++) {
+ views[i] = mPagedListView.findViewByPosition(i);
+ }
+ });
+ for (int i = 0; i < itemCount - 1; i++) {
+ assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
+ }
+
+ // Setting item spacing causes layout change.
+ // Implicitly wait for layout by making two calls in UI thread.
+ final int itemSpacing = 10;
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setItemSpacing(itemSpacing);
+ });
+ mActivityRule.runOnUiThread(() -> {
+ for (int i = 0; i < itemCount; i++) {
+ views[i] = mPagedListView.findViewByPosition(i);
+ }
+ });
+ for (int i = 0; i < itemCount - 1; i++) {
+ assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(itemSpacing)));
+ }
+
+ // Re-setting spacing back to 0 also works.
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setItemSpacing(0);
+ });
+ mActivityRule.runOnUiThread(() -> {
+ for (int i = 0; i < itemCount; i++) {
+ views[i] = mPagedListView.findViewByPosition(i);
+ }
+ });
+ for (int i = 0; i < itemCount - 1; i++) {
+ assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSetScrollBarButtonIcons() throws Throwable {
+ // Set up a pagedListView with a large item count to ensure the scroll bar buttons are
+ // always showing.
+ setUpPagedListView(100 /* itemCount */);
+
+ Drawable upDrawable = mActivity.getDrawable(R.drawable.ic_thumb_up);
+ mPagedListView.setUpButtonIcon(upDrawable);
+
+ ImageView upButton = mPagedListView.findViewById(R.id.page_up);
+ ViewMatchers.assertThat(upButton.getDrawable().getConstantState(),
+ is(equalTo(upDrawable.getConstantState())));
+
+ Drawable downDrawable = mActivity.getDrawable(R.drawable.ic_thumb_down);
+ mPagedListView.setDownButtonIcon(downDrawable);
+
+ ImageView downButton = mPagedListView.findViewById(R.id.page_down);
+ ViewMatchers.assertThat(downButton.getDrawable().getConstantState(),
+ is(equalTo(downDrawable.getConstantState())));
+ }
+
+ private static String itemText(int index) {
+ return "Data " + index;
+ }
+
+ private void verifyItemSnappedToListTop() {
+ int firstVisiblePosition = mPagedListView.getFirstFullyVisibleChildPosition();
+ if (firstVisiblePosition > 1) {
+ int lastInPreviousPagePosition = firstVisiblePosition - 1;
+ onView(withText(itemText(lastInPreviousPagePosition)))
+ .check(matches(not(isDisplayed())));
+ }
+ }
+
+ /** A base adapter that will handle inflating the test view and binding data to it. */
+ private abstract class BaseTestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+ protected List<String> mData;
+ protected int mParentHeight;
+
+ BaseTestAdapter(int itemCount, int parentHeight) {
+ mData = new ArrayList<>();
+ for (int i = 0; i < itemCount; i++) {
+ mData.add(itemText(i));
+ }
+ mParentHeight = parentHeight;
+ }
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ return new TestViewHolder(inflater, parent);
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder, int position) {
+ // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
+ int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
+ holder.itemView.setMinimumHeight(height);
+ holder.bind(mData.get(position));
+ }
+ }
+
+ private class TestAdapter extends BaseTestAdapter implements PagedListView.ItemCap {
+ private int mMaxItems;
+
+ TestAdapter(int itemCount, int parentHeight) {
+ super(itemCount, parentHeight);
+ }
+
+ @Override
+ public void setMaxItems(int maxItems) {
+ mMaxItems = maxItems;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mMaxItems > 0 ? Math.min(mData.size(), mMaxItems) : mData.size();
+ }
+ }
+
+ /**
+ * A variant of a {@link BaseTestAdapter} that does not implement {@link PagedListView.ItemCap}.
+ */
+ private class NoItemCapAdapter extends BaseTestAdapter {
+ NoItemCapAdapter(int itemCount, int parentHeight) {
+ super(itemCount, parentHeight);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+ }
+
+ private class TestViewHolder extends RecyclerView.ViewHolder {
+ private TextView mTextView;
+
+ TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
+ super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
+ mTextView = itemView.findViewById(R.id.text_view);
+ }
+
+ public void bind(String text) {
+ mTextView.setText(text);
+ }
+ }
+
+ private class PagedListViewScrollingIdlingResource implements IdlingResource {
+
+ private boolean mIdle = true;
+ private ResourceCallback mResourceCallback;
+
+ PagedListViewScrollingIdlingResource(PagedListView pagedListView) {
+ pagedListView.getRecyclerView().addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(
+ RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ mIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
+ // Treat dragging as idle, or Espresso will block itself when
+ // swiping.
+ || newState == RecyclerView.SCROLL_STATE_DRAGGING);
+ if (mIdle && mResourceCallback != null) {
+ mResourceCallback.onTransitionToIdle();
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ }
+ });
+ }
+
+ @Override
+ public String getName() {
+ return PagedListViewScrollingIdlingResource.class.getName();
+ }
+
+ @Override
+ public boolean isIdleNow() {
+ return mIdle;
+ }
+
+ @Override
+ public void registerIdleTransitionCallback(ResourceCallback callback) {
+ mResourceCallback = callback;
+ }
+ }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java b/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java
new file mode 100644
index 0000000..6371374
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.car.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.car.test.R;
+
+/**
+ * Simple test activity for {@link PagedListView} class.
+ *
+ */
+public class PagedListViewTestActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_paged_list_view);
+ }
+}
diff --git a/compat/api/current.txt b/compat/api/current.txt
index 96a94cb..5a87c03 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -678,6 +678,7 @@
ctor public ShortcutInfoCompat.Builder(android.content.Context, java.lang.String);
method public android.support.v4.content.pm.ShortcutInfoCompat build();
method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setActivity(android.content.ComponentName);
+ method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setAlwaysBadged();
method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setDisabledMessage(java.lang.CharSequence);
method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setIcon(android.support.v4.graphics.drawable.IconCompat);
method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setIntent(android.content.Intent);
diff --git a/compat/build.gradle b/compat/build.gradle
index 82d503c..b8ea13b 100644
--- a/compat/build.gradle
+++ b/compat/build.gradle
@@ -16,7 +16,9 @@
androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
- androidTestImplementation project(':support-testutils')
+ androidTestImplementation project(':support-testutils'), {
+ exclude group: 'com.android.support', module: 'support-compat'
+ }
}
android {
diff --git a/compat/src/main/java/android/support/v4/accessibilityservice/package.html b/compat/src/main/java/android/support/v4/accessibilityservice/package.html
deleted file mode 100755
index 3d017b0..0000000
--- a/compat/src/main/java/android/support/v4/accessibilityservice/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.accessibilityservice classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/app/package.html b/compat/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/compat/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later. The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/content/package.html b/compat/src/main/java/android/support/v4/content/package.html
deleted file mode 100755
index 33bf4b5..0000000
--- a/compat/src/main/java/android/support/v4/content/package.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<body>
-
-Support android.content classes to assist with development of applications for
-android API level 4 or later. The main features here are
-{@link android.support.v4.content.Loader} and related classes and
-{@link android.support.v4.content.LocalBroadcastManager} to
-provide a cleaner implementation of broadcasts that don't need to go outside
-of an app.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java b/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
index 3ae7470..63585e1 100644
--- a/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
+++ b/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
@@ -18,17 +18,20 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.graphics.drawable.IconCompat;
import android.text.TextUtils;
import java.util.Arrays;
/**
- * Helper for accessing features in {@link android.content.pm.ShortcutInfo}.
+ * Helper for accessing features in {@link ShortcutInfo}.
*/
public class ShortcutInfoCompat {
@@ -43,6 +46,7 @@
private CharSequence mDisabledMessage;
private IconCompat mIcon;
+ private boolean mIsAlwaysBadged;
private ShortcutInfoCompat() { }
@@ -69,11 +73,26 @@
return builder.build();
}
+ @VisibleForTesting
Intent addToIntent(Intent outIntent) {
outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1])
.putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString());
if (mIcon != null) {
- mIcon.addToShortcutIntent(outIntent);
+ Drawable badge = null;
+ if (mIsAlwaysBadged) {
+ PackageManager pm = mContext.getPackageManager();
+ if (mActivity != null) {
+ try {
+ badge = pm.getActivityIcon(mActivity);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore
+ }
+ }
+ if (badge == null) {
+ badge = mContext.getApplicationInfo().loadIcon(pm);
+ }
+ }
+ mIcon.addToShortcutIntent(outIntent, badge);
}
return outIntent;
}
@@ -250,7 +269,7 @@
* on the launcher.
*
* @see ShortcutInfo#getActivity()
- * @see android.content.pm.ShortcutInfo.Builder#setActivity(ComponentName)
+ * @see ShortcutInfo.Builder#setActivity(ComponentName)
*/
@NonNull
public Builder setActivity(@NonNull ComponentName activity) {
@@ -259,6 +278,23 @@
}
/**
+ * Badges the icon before passing it over to the Launcher.
+ * <p>
+ * Launcher automatically badges {@link ShortcutInfo}, so only the legacy shortcut icon,
+ * {@link Intent.ShortcutIconResource} is badged. This field is ignored when using
+ * {@link ShortcutInfo} on API 25 and above.
+ * <p>
+ * If the shortcut is associated with an activity, the activity icon is used as the badge,
+ * otherwise application icon is used.
+ *
+ * @see #setActivity(ComponentName)
+ */
+ public Builder setAlwaysBadged() {
+ mInfo.mIsAlwaysBadged = true;
+ return this;
+ }
+
+ /**
* Creates a {@link ShortcutInfoCompat} instance.
*/
@NonNull
diff --git a/compat/src/main/java/android/support/v4/content/pm/package.html b/compat/src/main/java/android/support/v4/content/pm/package.html
deleted file mode 100755
index da850bd..0000000
--- a/compat/src/main/java/android/support/v4/content/pm/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.content.pm classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/database/package.html b/compat/src/main/java/android/support/v4/database/package.html
deleted file mode 100755
index 25ac59a..0000000
--- a/compat/src/main/java/android/support/v4/database/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.database classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java b/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
index a2ad67f..359c96b 100644
--- a/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
@@ -18,6 +18,7 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
@@ -27,13 +28,17 @@
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.ContextCompat;
/**
* Helper for accessing features in {@link android.graphics.drawable.Icon}.
@@ -187,7 +192,8 @@
if (Build.VERSION.SDK_INT >= 26) {
return Icon.createWithAdaptiveBitmap((Bitmap) mObj1);
} else {
- return Icon.createWithBitmap(createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
+ return Icon.createWithBitmap(
+ createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, false));
}
case TYPE_RESOURCE:
return Icon.createWithResource((Context) mObj1, mInt1);
@@ -201,34 +207,74 @@
}
/**
+ * Use {@link #addToShortcutIntent(Intent, Drawable)} instead
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- public void addToShortcutIntent(Intent outIntent) {
+ @Deprecated
+ public void addToShortcutIntent(@NonNull Intent outIntent) {
+ addToShortcutIntent(outIntent, null);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void addToShortcutIntent(@NonNull Intent outIntent, @Nullable Drawable badge) {
+ Bitmap icon;
switch (mType) {
case TYPE_BITMAP:
- outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, (Bitmap) mObj1);
+ icon = (Bitmap) mObj1;
+ if (badge != null) {
+ // Do not modify the original icon when applying a badge
+ icon = icon.copy(icon.getConfig(), true);
+ }
break;
case TYPE_ADAPTIVE_BITMAP:
- outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
- createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
+ icon = createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, true);
break;
case TYPE_RESOURCE:
- outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1));
+ if (badge == null) {
+ outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1));
+ return;
+ } else {
+ Context context = (Context) mObj1;
+ Drawable dr = ContextCompat.getDrawable(context, mInt1);
+ if (dr.getIntrinsicWidth() <= 0 || dr.getIntrinsicHeight() <= 0) {
+ int size = ((ActivityManager) context.getSystemService(
+ Context.ACTIVITY_SERVICE)).getLauncherLargeIconSize();
+ icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ } else {
+ icon = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+ dr.setBounds(0, 0, icon.getWidth(), icon.getHeight());
+ dr.draw(new Canvas(icon));
+ }
break;
default:
throw new IllegalArgumentException("Icon type not supported for intent shortcuts");
}
+ if (badge != null) {
+ // Badge the icon
+ int w = icon.getWidth();
+ int h = icon.getHeight();
+ badge.setBounds(w / 2, h / 2, w, h);
+ badge.draw(new Canvas(icon));
+ }
+ outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
}
/**
* Converts a bitmap following the adaptive icon guide lines, into a bitmap following the
* shortcut icon guide lines.
* The returned bitmap will always have same width and height and clipped to a circle.
+ *
+ * @param addShadow set to {@code true} only for legacy shortcuts and {@code false} otherwise
*/
@VisibleForTesting
- static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap) {
+ static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap, boolean addShadow) {
int size = (int) (DEFAULT_VIEW_PORT_SCALE * Math.min(adaptiveIconBitmap.getWidth(),
adaptiveIconBitmap.getHeight()));
@@ -239,16 +285,18 @@
float center = size * 0.5f;
float radius = center * ICON_DIAMETER_FACTOR;
- // Draw key shadow
- float blur = BLUR_FACTOR * size;
- paint.setColor(Color.TRANSPARENT);
- paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24);
- canvas.drawCircle(center, center, radius, paint);
+ if (addShadow) {
+ // Draw key shadow
+ float blur = BLUR_FACTOR * size;
+ paint.setColor(Color.TRANSPARENT);
+ paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24);
+ canvas.drawCircle(center, center, radius, paint);
- // Draw ambient shadow
- paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
- canvas.drawCircle(center, center, radius, paint);
- paint.clearShadowLayer();
+ // Draw ambient shadow
+ paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
+ canvas.drawCircle(center, center, radius, paint);
+ paint.clearShadowLayer();
+ }
// Draw the clipped icon
paint.setColor(Color.BLACK);
diff --git a/compat/src/main/java/android/support/v4/os/package.html b/compat/src/main/java/android/support/v4/os/package.html
deleted file mode 100755
index 929c967..0000000
--- a/compat/src/main/java/android/support/v4/os/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.os classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/util/package.html b/compat/src/main/java/android/support/v4/util/package.html
deleted file mode 100644
index afde9b7..0000000
--- a/compat/src/main/java/android/support/v4/util/package.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/view/ViewCompat.java b/compat/src/main/java/android/support/v4/view/ViewCompat.java
index 34a198a..204a121 100644
--- a/compat/src/main/java/android/support/v4/view/ViewCompat.java
+++ b/compat/src/main/java/android/support/v4/view/ViewCompat.java
@@ -1356,7 +1356,7 @@
// after applying the tint
Drawable background = view.getBackground();
boolean hasTint = (view.getBackgroundTintList() != null)
- && (view.getBackgroundTintMode() != null);
+ || (view.getBackgroundTintMode() != null);
if ((background != null) && hasTint) {
if (background.isStateful()) {
background.setState(view.getDrawableState());
@@ -1375,7 +1375,7 @@
// after applying the tint
Drawable background = view.getBackground();
boolean hasTint = (view.getBackgroundTintList() != null)
- && (view.getBackgroundTintMode() != null);
+ || (view.getBackgroundTintMode() != null);
if ((background != null) && hasTint) {
if (background.isStateful()) {
background.setState(view.getDrawableState());
diff --git a/compat/src/main/java/android/support/v4/view/accessibility/package.html b/compat/src/main/java/android/support/v4/view/accessibility/package.html
deleted file mode 100755
index 57b084f..0000000
--- a/compat/src/main/java/android/support/v4/view/accessibility/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<body>
-
-Support classes to access some of the android.view.accessibility package features introduced after API level 4 in a backwards compatible fashion.
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/view/package.html b/compat/src/main/java/android/support/v4/view/package.html
deleted file mode 100755
index d80ef70..0000000
--- a/compat/src/main/java/android/support/v4/view/package.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later. The main features here are a variety of classes
-for handling backwards compatibility with views (for example
-{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch
-data if available), and a new
-{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over
-to the widget package).
-
-</body>
diff --git a/compat/src/main/java/android/support/v4/widget/package.html b/compat/src/main/java/android/support/v4/widget/package.html
deleted file mode 100755
index e2c636d..0000000
--- a/compat/src/main/java/android/support/v4/widget/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.widget classes to assist with development of applications for
-android API level 4 or later. This includes a complete modern implementation
-of {@link android.support.v4.widget.CursorAdapter} and related classes, which
-is needed for use with {@link android.support.v4.content.CursorLoader}.
-
-</body>
diff --git a/compat/tests/AndroidManifest.xml b/compat/tests/AndroidManifest.xml
index 4988845..ed6727f 100644
--- a/compat/tests/AndroidManifest.xml
+++ b/compat/tests/AndroidManifest.xml
@@ -37,7 +37,8 @@
<activity android:name="android.support.v4.view.ViewCompatActivity"/>
- <activity android:name="android.support.v4.app.TestSupportActivity"/>
+ <activity android:name="android.support.v4.app.TestSupportActivity"
+ android:icon="@drawable/test_drawable_blue"/>
<provider android:name="android.support.v4.provider.MockFontProvider"
android:authorities="android.support.provider.fonts.font"
diff --git a/compat/tests/java/android/support/v4/app/ActivityCompatTest.java b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
index 3b4f1b5..35889fb 100644
--- a/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
+++ b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
@@ -25,6 +25,7 @@
import android.Manifest;
import android.app.Activity;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.BaseInstrumentationTestCase;
@@ -40,6 +41,7 @@
super(TestSupportActivity.class);
}
+ @SdkSuppress(minSdkVersion = 24)
@SmallTest
@Test
public void testPermissionDelegate() {
diff --git a/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java b/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java
new file mode 100644
index 0000000..c1a5832
--- /dev/null
+++ b/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.content.pm;
+
+import static android.support.v4.graphics.drawable.IconCompatTest.verifyBadgeBitmap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.support.compat.test.R;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.TestSupportActivity;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.IconCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ShortcutInfoCompatTest {
+
+ private Intent mAction;
+
+ private Context mContext;
+ private ShortcutInfoCompat.Builder mBuilder;
+
+ @Before
+ public void setup() {
+ mContext = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+ mAction = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
+
+ mBuilder = new ShortcutInfoCompat.Builder(mContext, "test-shortcut")
+ .setIntent(mAction)
+ .setShortLabel("Test shortcut")
+ .setIcon(IconCompat.createWithResource(mContext, R.drawable.test_drawable_red));
+ }
+
+ @Test
+ public void testAddToIntent_noBadge() {
+ Intent intent = new Intent();
+ mBuilder.setActivity(new ComponentName(mContext, TestSupportActivity.class))
+ .build()
+ .addToIntent(intent);
+
+ assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+ assertNotNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+ assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+ }
+
+ @Test
+ public void testAddToIntent_badgeActivity() {
+ Intent intent = new Intent();
+ mBuilder.setActivity(new ComponentName(mContext, TestSupportActivity.class))
+ .setAlwaysBadged()
+ .build()
+ .addToIntent(intent);
+
+ assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+ assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+
+ verifyBadgeBitmap(intent, ContextCompat.getColor(mContext, R.color.test_red),
+ ContextCompat.getColor(mContext, R.color.test_blue));
+ }
+
+ @Test
+ public void testAddToIntent_badgeApplication() {
+ ApplicationInfo appInfo = spy(mContext.getApplicationInfo());
+ doReturn(ContextCompat.getDrawable(mContext, R.drawable.test_drawable_green))
+ .when(appInfo).loadIcon(any(PackageManager.class));
+ doReturn(appInfo).when(mContext).getApplicationInfo();
+
+ Intent intent = new Intent();
+ mBuilder.setAlwaysBadged()
+ .build()
+ .addToIntent(intent);
+
+ assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+ assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+
+ verifyBadgeBitmap(intent, ContextCompat.getColor(mContext, R.color.test_red),
+ ContextCompat.getColor(mContext, R.color.test_green));
+ }
+}
diff --git a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
index 3a48a6bd..7853f02 100644
--- a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
+++ b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
@@ -112,7 +113,7 @@
ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
doReturn(mockShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE));
when(mockShortcutManager.requestPinShortcut(
- any(ShortcutInfo.class), any(IntentSender.class))).thenReturn(true);
+ any(ShortcutInfo.class), nullable(IntentSender.class))).thenReturn(true);
assertTrue(ShortcutManagerCompat.requestPinShortcut(mContext, mInfoCompat, null));
ArgumentCaptor<ShortcutInfo> captor = ArgumentCaptor.forClass(ShortcutInfo.class);
diff --git a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
index d87ddac..c83ba7e 100644
--- a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
+++ b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
@@ -17,6 +17,9 @@
package android.support.v4.graphics.drawable;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.annotation.TargetApi;
@@ -34,6 +37,7 @@
import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.content.ContextCompat;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,7 +49,7 @@
@SmallTest
public class IconCompatTest {
- private void verifyClippedCircle(Bitmap bitmap, int fillColor, int size) {
+ private static void verifyClippedCircle(Bitmap bitmap, int fillColor, int size) {
assertEquals(size, bitmap.getHeight());
assertEquals(bitmap.getWidth(), bitmap.getHeight());
assertEquals(fillColor, bitmap.getPixel(size / 2, size / 2));
@@ -53,14 +57,28 @@
assertEquals(Color.TRANSPARENT, bitmap.getPixel(0, 0));
assertEquals(Color.TRANSPARENT, bitmap.getPixel(0, size - 1));
assertEquals(Color.TRANSPARENT, bitmap.getPixel(size - 1, 0));
+
+ // The badge is a full rectangle located at the bottom right corner. Check a single pixel
+ // in that region to verify that badging was properly applied.
assertEquals(Color.TRANSPARENT, bitmap.getPixel(size - 1, size - 1));
}
+ public static void verifyBadgeBitmap(Intent intent, int bgColor, int badgeColor) {
+ Bitmap bitmap = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ assertEquals(bgColor, bitmap.getPixel(2, 2));
+ assertEquals(bgColor, bitmap.getPixel(w - 2, 2));
+ assertEquals(bgColor, bitmap.getPixel(2, h - 2));
+ assertEquals(badgeColor, bitmap.getPixel(w - 2, h - 2));
+ }
+
@Test
public void testClipAdaptiveIcon() throws Throwable {
Bitmap source = Bitmap.createBitmap(200, 150, Bitmap.Config.ARGB_8888);
source.eraseColor(Color.RED);
- Bitmap result = IconCompat.createLegacyIconFromAdaptiveIcon(source);
+ Bitmap result = IconCompat.createLegacyIconFromAdaptiveIcon(source, false);
verifyClippedCircle(result, Color.RED, 100);
}
@@ -69,11 +87,46 @@
Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.RED);
Intent intent = new Intent();
- IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent);
+ IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent, null);
assertEquals(bitmap, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
}
@Test
+ public void testAddBitmapToShortcutIntent_badged() {
+ Context context = InstrumentationRegistry.getContext();
+ Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.RED);
+ Intent intent = new Intent();
+
+ Drawable badge = ContextCompat.getDrawable(context, R.drawable.test_drawable_blue);
+ IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent, badge);
+ assertNotSame(bitmap, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+
+ verifyBadgeBitmap(intent, Color.RED, ContextCompat.getColor(context, R.color.test_blue));
+ }
+
+ @Test
+ public void testAddResourceToShortcutIntent_badged() {
+ Context context = InstrumentationRegistry.getContext();
+ Intent intent = new Intent();
+
+ // No badge
+ IconCompat.createWithResource(context, R.drawable.test_drawable_green)
+ .addToShortcutIntent(intent, null);
+ assertNotNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+ assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+
+ intent = new Intent();
+ Drawable badge = ContextCompat.getDrawable(context, R.drawable.test_drawable_red);
+ IconCompat.createWithResource(context, R.drawable.test_drawable_blue)
+ .addToShortcutIntent(intent, badge);
+
+ assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+ verifyBadgeBitmap(intent, ContextCompat.getColor(context, R.color.test_blue),
+ ContextCompat.getColor(context, R.color.test_red));
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
@TargetApi(Build.VERSION_CODES.M)
public void testCreateWithBitmap() {
@@ -90,7 +143,7 @@
Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.GREEN);
Intent intent = new Intent();
- IconCompat.createWithAdaptiveBitmap(bitmap).addToShortcutIntent(intent);
+ IconCompat.createWithAdaptiveBitmap(bitmap).addToShortcutIntent(intent, null);
Bitmap clipped = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
verifyClippedCircle(clipped, Color.GREEN, clipped.getWidth());
diff --git a/content/OWNERS b/content/OWNERS
new file mode 100644
index 0000000..779e918
--- /dev/null
+++ b/content/OWNERS
@@ -0,0 +1 @@
+smckay@google.com
\ No newline at end of file
diff --git a/core-ui/Android.mk b/core-ui/Android.mk
index 184d7be..47846a9 100644
--- a/core-ui/Android.mk
+++ b/core-ui/Android.mk
@@ -30,6 +30,7 @@
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_SHARED_ANDROID_LIBRARIES := \
android-support-compat \
+ android-support-core-utils \
android-support-annotations
LOCAL_JAR_EXCLUDE_FILES := none
LOCAL_JAVA_LANGUAGE_VERSION := 1.7
diff --git a/core-ui/api/current.txt b/core-ui/api/current.txt
index 6ae4b1a..346ffc4 100644
--- a/core-ui/api/current.txt
+++ b/core-ui/api/current.txt
@@ -1,3 +1,97 @@
+package android.support.design.widget {
+
+ public class CoordinatorLayout extends android.view.ViewGroup {
+ ctor public CoordinatorLayout(android.content.Context);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet);
+ ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet, int);
+ method public void dispatchDependentViewsChanged(android.view.View);
+ method public boolean doViewsOverlap(android.view.View, android.view.View);
+ method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateDefaultLayoutParams();
+ method public android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.util.AttributeSet);
+ method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
+ method public java.util.List<android.view.View> getDependencies(android.view.View);
+ method public java.util.List<android.view.View> getDependents(android.view.View);
+ method public android.graphics.drawable.Drawable getStatusBarBackground();
+ method public boolean isPointInChildBounds(android.view.View, int, int);
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onDraw(android.graphics.Canvas);
+ method protected void onLayout(boolean, int, int, int, int);
+ method public void onLayoutChild(android.view.View, int);
+ method public void onMeasureChild(android.view.View, int, int, int, int);
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
+ method public void setStatusBarBackground(android.graphics.drawable.Drawable);
+ method public void setStatusBarBackgroundColor(int);
+ method public void setStatusBarBackgroundResource(int);
+ }
+
+ public static abstract class CoordinatorLayout.Behavior<V extends android.view.View> {
+ ctor public CoordinatorLayout.Behavior();
+ ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet);
+ method public boolean blocksInteractionBelow(android.support.design.widget.CoordinatorLayout, V);
+ method public boolean getInsetDodgeRect(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect);
+ method public int getScrimColor(android.support.design.widget.CoordinatorLayout, V);
+ method public float getScrimOpacity(android.support.design.widget.CoordinatorLayout, V);
+ method public static java.lang.Object getTag(android.view.View);
+ method public boolean layoutDependsOn(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+ method public android.support.v4.view.WindowInsetsCompat onApplyWindowInsets(android.support.design.widget.CoordinatorLayout, V, android.support.v4.view.WindowInsetsCompat);
+ method public void onAttachedToLayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
+ method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDependentViewRemoved(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+ method public void onDetachedFromLayoutParams();
+ method public boolean onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, V, int);
+ method public boolean onMeasureChild(android.support.design.widget.CoordinatorLayout, V, int, int, int, int);
+ method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+ method public boolean onNestedPreFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float);
+ method public deprecated void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+ method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
+ method public deprecated void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+ method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
+ method public deprecated void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+ method public void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+ method public boolean onRequestChildRectangleOnScreen(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+ method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, V, android.os.Parcelable);
+ method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, V);
+ method public deprecated boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+ method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+ method public deprecated void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+ method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int);
+ method public boolean onTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
+ method public static void setTag(android.view.View, java.lang.Object);
+ }
+
+ public static abstract class CoordinatorLayout.DefaultBehavior implements java.lang.annotation.Annotation {
+ }
+
+ public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ctor public CoordinatorLayout.LayoutParams(int, int);
+ ctor public CoordinatorLayout.LayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+ ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+ method public int getAnchorId();
+ method public android.support.design.widget.CoordinatorLayout.Behavior getBehavior();
+ method public void setAnchorId(int);
+ method public void setBehavior(android.support.design.widget.CoordinatorLayout.Behavior);
+ field public int anchorGravity;
+ field public int dodgeInsetEdges;
+ field public int gravity;
+ field public int insetEdge;
+ field public int keyline;
+ }
+
+ protected static class CoordinatorLayout.SavedState extends android.support.v4.view.AbsSavedState {
+ ctor public CoordinatorLayout.SavedState(android.os.Parcel, java.lang.ClassLoader);
+ ctor public CoordinatorLayout.SavedState(android.os.Parcelable);
+ field public static final android.os.Parcelable.Creator<android.support.design.widget.CoordinatorLayout.SavedState> CREATOR;
+ }
+
+}
+
package android.support.v4.app {
public deprecated class ActionBarDrawerToggle implements android.support.v4.widget.DrawerLayout.DrawerListener {
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index cd70447..098440d 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -8,12 +8,18 @@
dependencies {
api project(':support-annotations')
api project(':support-compat')
+ api project(':support-core-utils')
androidTestImplementation libs.test_runner, { exclude module: 'support-annotations' }
androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
+ androidTestImplementation libs.espresso_contrib, { exclude group: 'com.android.support' }
androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
- androidTestImplementation project(':support-testutils')
+ androidTestImplementation project(':support-testutils'), {
+ exclude group: 'com.android.support', module: 'support-core-ui'
+ }
+
+ testImplementation libs.junit
}
android {
@@ -21,6 +27,13 @@
minSdkVersion 14
}
+ sourceSets {
+ main.res.srcDirs = [
+ 'res',
+ 'res-public'
+ ]
+ }
+
buildTypes.all {
consumerProguardFiles 'proguard-rules.pro'
}
diff --git a/core-ui/proguard-rules.pro b/core-ui/proguard-rules.pro
index 2ec1c65..cbf4e1f 100644
--- a/core-ui/proguard-rules.pro
+++ b/core-ui/proguard-rules.pro
@@ -12,5 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# Make sure we keep annotations for ViewPager's DecorView
+# CoordinatorLayout resolves the behaviors of its child components with reflection.
+-keep public class * extends android.support.design.widget.CoordinatorLayout$Behavior {
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>();
+}
+
+# Make sure we keep annotations for CoordinatorLayout's DefaultBehavior and ViewPager's DecorView
-keepattributes *Annotation*
diff --git a/core-ui/res-public/values/public_attrs.xml b/core-ui/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..505d55b
--- /dev/null
+++ b/core-ui/res-public/values/public_attrs.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Definitions of attributes to be exposed as public -->
+<resources>
+ <public type="attr" name="keylines"/>
+ <public type="attr" name="layout_anchor"/>
+ <public type="attr" name="layout_anchorGravity"/>
+ <public type="attr" name="layout_behavior"/>
+ <public type="attr" name="layout_dodgeInsetEdges"/>
+ <public type="attr" name="layout_insetEdge"/>
+ <public type="attr" name="layout_keyline"/>
+ <public type="attr" name="statusBarBackground"/>
+</resources>
diff --git a/core-ui/res-public/values/public_styles.xml b/core-ui/res-public/values/public_styles.xml
new file mode 100644
index 0000000..f9b6bab
--- /dev/null
+++ b/core-ui/res-public/values/public_styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- Definitions of styles to be exposed as public -->
+<resources>
+ <public type="style" name="Widget.Support.CoordinatorLayout"/>
+</resources>
diff --git a/core-ui/res/values/attrs.xml b/core-ui/res/values/attrs.xml
new file mode 100644
index 0000000..b535c45
--- /dev/null
+++ b/core-ui/res/values/attrs.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- Style to use for coordinator layouts. -->
+ <attr name="coordinatorLayoutStyle" format="reference" />
+
+ <declare-styleable name="CoordinatorLayout">
+ <!-- A reference to an array of integers representing the
+ locations of horizontal keylines in dp from the starting edge.
+ Child views can refer to these keylines for alignment using
+ layout_keyline="index" where index is a 0-based index into
+ this array. -->
+ <attr name="keylines" format="reference"/>
+ <!-- Drawable to display behind the status bar when the view is set to draw behind it. -->
+ <attr name="statusBarBackground" format="color|reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="CoordinatorLayout_Layout">
+ <attr name="android:layout_gravity"/>
+ <!-- The class name of a Behavior class defining special runtime behavior
+ for this child view. -->
+ <attr name="layout_behavior" format="string"/>
+ <!-- The id of an anchor view that this view should position relative to. -->
+ <attr name="layout_anchor" format="reference"/>
+ <!-- The index of a keyline this view should position relative to.
+ android:layout_gravity will affect how the view aligns to the
+ specified keyline. -->
+ <attr name="layout_keyline" format="integer"/>
+
+ <!-- Specifies how an object should position relative to an anchor, on both the X and Y axes,
+ within its parent's bounds. -->
+ <attr name="layout_anchorGravity">
+ <!-- Push object to the top of its container, not changing its size. -->
+ <flag name="top" value="0x30"/>
+ <!-- Push object to the bottom of its container, not changing its size. -->
+ <flag name="bottom" value="0x50"/>
+ <!-- Push object to the left of its container, not changing its size. -->
+ <flag name="left" value="0x03"/>
+ <!-- Push object to the right of its container, not changing its size. -->
+ <flag name="right" value="0x05"/>
+ <!-- Place object in the vertical center of its container, not changing its size. -->
+ <flag name="center_vertical" value="0x10"/>
+ <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill_vertical" value="0x70"/>
+ <!-- Place object in the horizontal center of its container, not changing its size. -->
+ <flag name="center_horizontal" value="0x01"/>
+ <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
+ <flag name="fill_horizontal" value="0x07"/>
+ <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
+ <flag name="center" value="0x11"/>
+ <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill" value="0x77"/>
+ <!-- Additional option that can be set to have the top and/or bottom edges of
+ the child clipped to its container's bounds.
+ The clip will be based on the vertical gravity: a top gravity will clip the bottom
+ edge, a bottom gravity will clip the top edge, and neither will clip both edges. -->
+ <flag name="clip_vertical" value="0x80"/>
+ <!-- Additional option that can be set to have the left and/or right edges of
+ the child clipped to its container's bounds.
+ The clip will be based on the horizontal gravity: a left gravity will clip the right
+ edge, a right gravity will clip the left edge, and neither will clip both edges. -->
+ <flag name="clip_horizontal" value="0x08"/>
+ <!-- Push object to the beginning of its container, not changing its size. -->
+ <flag name="start" value="0x00800003"/>
+ <!-- Push object to the end of its container, not changing its size. -->
+ <flag name="end" value="0x00800005"/>
+ </attr>
+
+ <!-- Specifies how this view insets the CoordinatorLayout and make some other views
+ dodge it. -->
+ <attr name="layout_insetEdge" format="enum">
+ <!-- Don't inset. -->
+ <enum name="none" value="0x0"/>
+ <!-- Inset the top edge. -->
+ <enum name="top" value="0x30"/>
+ <!-- Inset the bottom edge. -->
+ <enum name="bottom" value="0x50"/>
+ <!-- Inset the left edge. -->
+ <enum name="left" value="0x03"/>
+ <!-- Inset the right edge. -->
+ <enum name="right" value="0x05"/>
+ <!-- Inset the start edge. -->
+ <enum name="start" value="0x00800003"/>
+ <!-- Inset the end edge. -->
+ <enum name="end" value="0x00800005"/>
+ </attr>
+ <!-- Specifies how this view dodges the inset edges of the CoordinatorLayout. -->
+ <attr name="layout_dodgeInsetEdges">
+ <!-- Don't dodge any edges -->
+ <flag name="none" value="0x0"/>
+ <!-- Dodge the top inset edge. -->
+ <flag name="top" value="0x30"/>
+ <!-- Dodge the bottom inset edge. -->
+ <flag name="bottom" value="0x50"/>
+ <!-- Dodge the left inset edge. -->
+ <flag name="left" value="0x03"/>
+ <!-- Dodge the right inset edge. -->
+ <flag name="right" value="0x05"/>
+ <!-- Dodge the start inset edge. -->
+ <flag name="start" value="0x00800003"/>
+ <!-- Dodge the end inset edge. -->
+ <flag name="end" value="0x00800005"/>
+ <!-- Dodge all the inset edges. -->
+ <flag name="all" value="0x77"/>
+ </attr>
+ </declare-styleable>
+</resources>
diff --git a/core-ui/res/values/styles.xml b/core-ui/res/values/styles.xml
new file mode 100644
index 0000000..07fdbc5
--- /dev/null
+++ b/core-ui/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="Widget.Support.CoordinatorLayout" parent="android:Widget">
+ <item name="statusBarBackground">#000000</item>
+ </style>
+</resources>
diff --git a/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
new file mode 100644
index 0000000..c45810e
--- /dev/null
+++ b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
@@ -0,0 +1,3249 @@
+/*
+ * 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 android.support.design.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+import android.support.coreui.R;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.math.MathUtils;
+import android.support.v4.util.ObjectsCompat;
+import android.support.v4.util.Pools;
+import android.support.v4.view.AbsSavedState;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.view.NestedScrollingParent;
+import android.support.v4.view.NestedScrollingParent2;
+import android.support.v4.view.NestedScrollingParentHelper;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewCompat.NestedScrollType;
+import android.support.v4.view.ViewCompat.ScrollAxis;
+import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.DirectedAcyclicGraph;
+import android.support.v4.widget.ViewGroupUtils;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CoordinatorLayout is a super-powered {@link android.widget.FrameLayout FrameLayout}.
+ *
+ * <p>CoordinatorLayout is intended for two primary use cases:</p>
+ * <ol>
+ * <li>As a top-level application decor or chrome layout</li>
+ * <li>As a container for a specific interaction with one or more child views</li>
+ * </ol>
+ *
+ * <p>By specifying {@link Behavior Behaviors} for child views of a
+ * CoordinatorLayout you can provide many different interactions within a single parent and those
+ * views can also interact with one another. View classes can specify a default behavior when
+ * used as a child of a CoordinatorLayout using the
+ * {@link DefaultBehavior} annotation.</p>
+ *
+ * <p>Behaviors may be used to implement a variety of interactions and additional layout
+ * modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons
+ * that stick to other elements as they move and animate.</p>
+ *
+ * <p>Children of a CoordinatorLayout may have an
+ * {@link LayoutParams#setAnchorId(int) anchor}. This view id must correspond
+ * to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself
+ * or a descendant of the anchored child. This can be used to place floating views relative to
+ * other arbitrary content panes.</p>
+ *
+ * <p>Children can specify {@link LayoutParams#insetEdge} to describe how the
+ * view insets the CoordinatorLayout. Any child views which are set to dodge the same inset edges by
+ * {@link LayoutParams#dodgeInsetEdges} will be moved appropriately so that the
+ * views do not overlap.</p>
+ */
+public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
+ static final String TAG = "CoordinatorLayout";
+ static final String WIDGET_PACKAGE_NAME;
+
+ static {
+ final Package pkg = CoordinatorLayout.class.getPackage();
+ WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null;
+ }
+
+ private static final int TYPE_ON_INTERCEPT = 0;
+ private static final int TYPE_ON_TOUCH = 1;
+
+ static {
+ if (Build.VERSION.SDK_INT >= 21) {
+ TOP_SORTED_CHILDREN_COMPARATOR = new ViewElevationComparator();
+ } else {
+ TOP_SORTED_CHILDREN_COMPARATOR = null;
+ }
+ }
+
+ static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
+ Context.class,
+ AttributeSet.class
+ };
+
+ static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors =
+ new ThreadLocal<>();
+
+ static final int EVENT_PRE_DRAW = 0;
+ static final int EVENT_NESTED_SCROLL = 1;
+ static final int EVENT_VIEW_REMOVED = 2;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({EVENT_PRE_DRAW, EVENT_NESTED_SCROLL, EVENT_VIEW_REMOVED})
+ public @interface DispatchChangeEvent {}
+
+ static final Comparator<View> TOP_SORTED_CHILDREN_COMPARATOR;
+ private static final Pools.Pool<Rect> sRectPool = new Pools.SynchronizedPool<>(12);
+
+ @NonNull
+ private static Rect acquireTempRect() {
+ Rect rect = sRectPool.acquire();
+ if (rect == null) {
+ rect = new Rect();
+ }
+ return rect;
+ }
+
+ private static void releaseTempRect(@NonNull Rect rect) {
+ rect.setEmpty();
+ sRectPool.release(rect);
+ }
+
+ private final List<View> mDependencySortedChildren = new ArrayList<>();
+ private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
+
+ private final List<View> mTempList1 = new ArrayList<>();
+ private final List<View> mTempDependenciesList = new ArrayList<>();
+ private final int[] mTempIntPair = new int[2];
+ private Paint mScrimPaint;
+
+ private boolean mDisallowInterceptReset;
+
+ private boolean mIsAttachedToWindow;
+
+ private int[] mKeylines;
+
+ private View mBehaviorTouchView;
+ private View mNestedScrollingTarget;
+
+ private OnPreDrawListener mOnPreDrawListener;
+ private boolean mNeedsPreDrawListener;
+
+ private WindowInsetsCompat mLastInsets;
+ private boolean mDrawStatusBarBackground;
+ private Drawable mStatusBarBackground;
+
+ OnHierarchyChangeListener mOnHierarchyChangeListener;
+ private android.support.v4.view.OnApplyWindowInsetsListener mApplyWindowInsetsListener;
+
+ private final NestedScrollingParentHelper mNestedScrollingParentHelper =
+ new NestedScrollingParentHelper(this);
+
+ public CoordinatorLayout(Context context) {
+ this(context, null);
+ }
+
+ public CoordinatorLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.coordinatorLayoutStyle);
+ }
+
+ public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray a = (defStyleAttr == 0)
+ ? context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
+ 0, R.style.Widget_Support_CoordinatorLayout)
+ : context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
+ defStyleAttr, 0);
+ final int keylineArrayRes = a.getResourceId(R.styleable.CoordinatorLayout_keylines, 0);
+ if (keylineArrayRes != 0) {
+ final Resources res = context.getResources();
+ mKeylines = res.getIntArray(keylineArrayRes);
+ final float density = res.getDisplayMetrics().density;
+ final int count = mKeylines.length;
+ for (int i = 0; i < count; i++) {
+ mKeylines[i] = (int) (mKeylines[i] * density);
+ }
+ }
+ mStatusBarBackground = a.getDrawable(R.styleable.CoordinatorLayout_statusBarBackground);
+ a.recycle();
+
+ setupForInsets();
+ super.setOnHierarchyChangeListener(new HierarchyChangeListener());
+ }
+
+ @Override
+ public void setOnHierarchyChangeListener(OnHierarchyChangeListener onHierarchyChangeListener) {
+ mOnHierarchyChangeListener = onHierarchyChangeListener;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ resetTouchBehaviors(false);
+ if (mNeedsPreDrawListener) {
+ if (mOnPreDrawListener == null) {
+ mOnPreDrawListener = new OnPreDrawListener();
+ }
+ final ViewTreeObserver vto = getViewTreeObserver();
+ vto.addOnPreDrawListener(mOnPreDrawListener);
+ }
+ if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
+ // We're set to fitSystemWindows but we haven't had any insets yet...
+ // We should request a new dispatch of window insets
+ ViewCompat.requestApplyInsets(this);
+ }
+ mIsAttachedToWindow = true;
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ resetTouchBehaviors(false);
+ if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
+ final ViewTreeObserver vto = getViewTreeObserver();
+ vto.removeOnPreDrawListener(mOnPreDrawListener);
+ }
+ if (mNestedScrollingTarget != null) {
+ onStopNestedScroll(mNestedScrollingTarget);
+ }
+ mIsAttachedToWindow = false;
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar.
+ * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
+ *
+ * @param bg Background drawable to draw behind the status bar
+ */
+ public void setStatusBarBackground(@Nullable final Drawable bg) {
+ if (mStatusBarBackground != bg) {
+ if (mStatusBarBackground != null) {
+ mStatusBarBackground.setCallback(null);
+ }
+ mStatusBarBackground = bg != null ? bg.mutate() : null;
+ if (mStatusBarBackground != null) {
+ if (mStatusBarBackground.isStateful()) {
+ mStatusBarBackground.setState(getDrawableState());
+ }
+ DrawableCompat.setLayoutDirection(mStatusBarBackground,
+ ViewCompat.getLayoutDirection(this));
+ mStatusBarBackground.setVisible(getVisibility() == VISIBLE, false);
+ mStatusBarBackground.setCallback(this);
+ }
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ /**
+ * Gets the drawable used to draw in the insets area for the status bar.
+ *
+ * @return The status bar background drawable, or null if none set
+ */
+ @Nullable
+ public Drawable getStatusBarBackground() {
+ return mStatusBarBackground;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final int[] state = getDrawableState();
+ boolean changed = false;
+
+ Drawable d = mStatusBarBackground;
+ if (d != null && d.isStateful()) {
+ changed |= d.setState(state);
+ }
+
+ if (changed) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mStatusBarBackground;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ final boolean visible = visibility == VISIBLE;
+ if (mStatusBarBackground != null && mStatusBarBackground.isVisible() != visible) {
+ mStatusBarBackground.setVisible(visible, false);
+ }
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar.
+ * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
+ *
+ * @param resId Resource id of a background drawable to draw behind the status bar
+ */
+ public void setStatusBarBackgroundResource(@DrawableRes int resId) {
+ setStatusBarBackground(resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null);
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar.
+ * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
+ *
+ * @param color Color to use as a background drawable to draw behind the status bar
+ * in 0xAARRGGBB format.
+ */
+ public void setStatusBarBackgroundColor(@ColorInt int color) {
+ setStatusBarBackground(new ColorDrawable(color));
+ }
+
+ final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
+ if (!ObjectsCompat.equals(mLastInsets, insets)) {
+ mLastInsets = insets;
+ mDrawStatusBarBackground = insets != null && insets.getSystemWindowInsetTop() > 0;
+ setWillNotDraw(!mDrawStatusBarBackground && getBackground() == null);
+
+ // Now dispatch to the Behaviors
+ insets = dispatchApplyWindowInsetsToBehaviors(insets);
+ requestLayout();
+ }
+ return insets;
+ }
+
+ final WindowInsetsCompat getLastWindowInsets() {
+ return mLastInsets;
+ }
+
+ /**
+ * Reset all Behavior-related tracking records either to clean up or in preparation
+ * for a new event stream. This should be called when attached or detached from a window,
+ * in response to an UP or CANCEL event, when intercept is request-disallowed
+ * and similar cases where an event stream in progress will be aborted.
+ */
+ private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+ if (b != null) {
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ if (notifyOnInterceptTouchEvent) {
+ b.onInterceptTouchEvent(this, child, cancelEvent);
+ } else {
+ b.onTouchEvent(this, child, cancelEvent);
+ }
+ cancelEvent.recycle();
+ }
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.resetTouchBehaviorTracking();
+ }
+ mDisallowInterceptReset = false;
+ }
+
+ /**
+ * Populate a list with the current child views, sorted such that the topmost views
+ * in z-order are at the front of the list. Useful for hit testing and event dispatch.
+ */
+ private void getTopSortedChildren(List<View> out) {
+ out.clear();
+
+ final boolean useCustomOrder = isChildrenDrawingOrderEnabled();
+ final int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final int childIndex = useCustomOrder ? getChildDrawingOrder(childCount, i) : i;
+ final View child = getChildAt(childIndex);
+ out.add(child);
+ }
+
+ if (TOP_SORTED_CHILDREN_COMPARATOR != null) {
+ Collections.sort(out, TOP_SORTED_CHILDREN_COMPARATOR);
+ }
+ }
+
+ private boolean performIntercept(MotionEvent ev, final int type) {
+ boolean intercepted = false;
+ boolean newBlock = false;
+
+ MotionEvent cancelEvent = null;
+
+ final int action = ev.getActionMasked();
+
+ final List<View> topmostChildList = mTempList1;
+ getTopSortedChildren(topmostChildList);
+
+ // Let topmost child views inspect first
+ final int childCount = topmostChildList.size();
+ for (int i = 0; i < childCount; i++) {
+ final View child = topmostChildList.get(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+
+ if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
+ // Cancel all behaviors beneath the one that intercepted.
+ // If the event is "down" then we don't have anything to cancel yet.
+ if (b != null) {
+ if (cancelEvent == null) {
+ final long now = SystemClock.uptimeMillis();
+ cancelEvent = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ }
+ switch (type) {
+ case TYPE_ON_INTERCEPT:
+ b.onInterceptTouchEvent(this, child, cancelEvent);
+ break;
+ case TYPE_ON_TOUCH:
+ b.onTouchEvent(this, child, cancelEvent);
+ break;
+ }
+ }
+ continue;
+ }
+
+ if (!intercepted && b != null) {
+ switch (type) {
+ case TYPE_ON_INTERCEPT:
+ intercepted = b.onInterceptTouchEvent(this, child, ev);
+ break;
+ case TYPE_ON_TOUCH:
+ intercepted = b.onTouchEvent(this, child, ev);
+ break;
+ }
+ if (intercepted) {
+ mBehaviorTouchView = child;
+ }
+ }
+
+ // Don't keep going if we're not allowing interaction below this.
+ // Setting newBlock will make sure we cancel the rest of the behaviors.
+ final boolean wasBlocking = lp.didBlockInteraction();
+ final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
+ newBlock = isBlocking && !wasBlocking;
+ if (isBlocking && !newBlock) {
+ // Stop here since we don't have anything more to cancel - we already did
+ // when the behavior first started blocking things below this point.
+ break;
+ }
+ }
+
+ topmostChildList.clear();
+
+ return intercepted;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ MotionEvent cancelEvent = null;
+
+ final int action = ev.getActionMasked();
+
+ // Make sure we reset in case we had missed a previous important event.
+ if (action == MotionEvent.ACTION_DOWN) {
+ resetTouchBehaviors(true);
+ }
+
+ final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
+
+ if (cancelEvent != null) {
+ cancelEvent.recycle();
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ resetTouchBehaviors(true);
+ }
+
+ return intercepted;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean handled = false;
+ boolean cancelSuper = false;
+ MotionEvent cancelEvent = null;
+
+ final int action = ev.getActionMasked();
+
+ if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
+ // Safe since performIntercept guarantees that
+ // mBehaviorTouchView != null if it returns true
+ final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+ if (b != null) {
+ handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
+ }
+ }
+
+ // Keep the super implementation correct
+ if (mBehaviorTouchView == null) {
+ handled |= super.onTouchEvent(ev);
+ } else if (cancelSuper) {
+ if (cancelEvent == null) {
+ final long now = SystemClock.uptimeMillis();
+ cancelEvent = MotionEvent.obtain(now, now,
+ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ }
+ super.onTouchEvent(cancelEvent);
+ }
+
+ if (!handled && action == MotionEvent.ACTION_DOWN) {
+
+ }
+
+ if (cancelEvent != null) {
+ cancelEvent.recycle();
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ resetTouchBehaviors(false);
+ }
+
+ return handled;
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ if (disallowIntercept && !mDisallowInterceptReset) {
+ resetTouchBehaviors(false);
+ mDisallowInterceptReset = true;
+ }
+ }
+
+ private int getKeyline(int index) {
+ if (mKeylines == null) {
+ Log.e(TAG, "No keylines defined for " + this + " - attempted index lookup " + index);
+ return 0;
+ }
+
+ if (index < 0 || index >= mKeylines.length) {
+ Log.e(TAG, "Keyline index " + index + " out of range for " + this);
+ return 0;
+ }
+
+ return mKeylines[index];
+ }
+
+ static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
+ if (TextUtils.isEmpty(name)) {
+ return null;
+ }
+
+ final String fullName;
+ if (name.startsWith(".")) {
+ // Relative to the app package. Prepend the app package name.
+ fullName = context.getPackageName() + name;
+ } else if (name.indexOf('.') >= 0) {
+ // Fully qualified package name.
+ fullName = name;
+ } else {
+ // Assume stock behavior in this package (if we have one)
+ fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
+ ? (WIDGET_PACKAGE_NAME + '.' + name)
+ : name;
+ }
+
+ try {
+ Map<String, Constructor<Behavior>> constructors = sConstructors.get();
+ if (constructors == null) {
+ constructors = new HashMap<>();
+ sConstructors.set(constructors);
+ }
+ Constructor<Behavior> c = constructors.get(fullName);
+ if (c == null) {
+ final Class<Behavior> clazz = (Class<Behavior>) context.getClassLoader()
+ .loadClass(fullName);
+ c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
+ c.setAccessible(true);
+ constructors.put(fullName, c);
+ }
+ return c.newInstance(context, attrs);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
+ }
+ }
+
+ LayoutParams getResolvedLayoutParams(View child) {
+ final LayoutParams result = (LayoutParams) child.getLayoutParams();
+ if (!result.mBehaviorResolved) {
+ Class<?> childClass = child.getClass();
+ DefaultBehavior defaultBehavior = null;
+ while (childClass != null &&
+ (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
+ childClass = childClass.getSuperclass();
+ }
+ if (defaultBehavior != null) {
+ try {
+ result.setBehavior(
+ defaultBehavior.value().getDeclaredConstructor().newInstance());
+ } catch (Exception e) {
+ Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
+ " could not be instantiated. Did you forget a default constructor?", e);
+ }
+ }
+ result.mBehaviorResolved = true;
+ }
+ return result;
+ }
+
+ private void prepareChildren() {
+ mDependencySortedChildren.clear();
+ mChildDag.clear();
+
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ final View view = getChildAt(i);
+
+ final LayoutParams lp = getResolvedLayoutParams(view);
+ lp.findAnchorView(this, view);
+
+ mChildDag.addNode(view);
+
+ // Now iterate again over the other children, adding any dependencies to the graph
+ for (int j = 0; j < count; j++) {
+ if (j == i) {
+ continue;
+ }
+ final View other = getChildAt(j);
+ if (lp.dependsOn(this, view, other)) {
+ if (!mChildDag.contains(other)) {
+ // Make sure that the other node is added
+ mChildDag.addNode(other);
+ }
+ // Now add the dependency to the graph
+ mChildDag.addEdge(other, view);
+ }
+ }
+ }
+
+ // Finally add the sorted graph list to our list
+ mDependencySortedChildren.addAll(mChildDag.getSortedList());
+ // We also need to reverse the result since we want the start of the list to contain
+ // Views which have no dependencies, then dependent views after that
+ Collections.reverse(mDependencySortedChildren);
+ }
+
+ /**
+ * Retrieve the transformed bounding rect of an arbitrary descendant view.
+ * This does not need to be a direct child.
+ *
+ * @param descendant descendant view to reference
+ * @param out rect to set to the bounds of the descendant view
+ */
+ void getDescendantRect(View descendant, Rect out) {
+ ViewGroupUtils.getDescendantRect(this, descendant, out);
+ }
+
+ @Override
+ protected int getSuggestedMinimumWidth() {
+ return Math.max(super.getSuggestedMinimumWidth(), getPaddingLeft() + getPaddingRight());
+ }
+
+ @Override
+ protected int getSuggestedMinimumHeight() {
+ return Math.max(super.getSuggestedMinimumHeight(), getPaddingTop() + getPaddingBottom());
+ }
+
+ /**
+ * Called to measure each individual child view unless a
+ * {@link Behavior Behavior} is present. The Behavior may choose to delegate
+ * child measurement to this method.
+ *
+ * @param child the child to measure
+ * @param parentWidthMeasureSpec the width requirements for this view
+ * @param widthUsed extra space that has been used up by the parent
+ * horizontally (possibly by other children of the parent)
+ * @param parentHeightMeasureSpec the height requirements for this view
+ * @param heightUsed extra space that has been used up by the parent
+ * vertically (possibly by other children of the parent)
+ */
+ public void onMeasureChild(View child, int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ prepareChildren();
+ ensurePreDrawListener();
+
+ final int paddingLeft = getPaddingLeft();
+ final int paddingTop = getPaddingTop();
+ final int paddingRight = getPaddingRight();
+ final int paddingBottom = getPaddingBottom();
+ final int layoutDirection = ViewCompat.getLayoutDirection(this);
+ final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ final int widthPadding = paddingLeft + paddingRight;
+ final int heightPadding = paddingTop + paddingBottom;
+ int widthUsed = getSuggestedMinimumWidth();
+ int heightUsed = getSuggestedMinimumHeight();
+ int childState = 0;
+
+ final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
+
+ final int childCount = mDependencySortedChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ final View child = mDependencySortedChildren.get(i);
+ if (child.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int keylineWidthUsed = 0;
+ if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
+ final int keylinePos = getKeyline(lp.keyline);
+ final int keylineGravity = GravityCompat.getAbsoluteGravity(
+ resolveKeylineGravity(lp.gravity), layoutDirection)
+ & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if ((keylineGravity == Gravity.LEFT && !isRtl)
+ || (keylineGravity == Gravity.RIGHT && isRtl)) {
+ keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
+ } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
+ || (keylineGravity == Gravity.LEFT && isRtl)) {
+ keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
+ }
+ }
+
+ int childWidthMeasureSpec = widthMeasureSpec;
+ int childHeightMeasureSpec = heightMeasureSpec;
+ if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
+ // We're set to handle insets but this child isn't, so we will measure the
+ // child as if there are no insets
+ final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
+ + mLastInsets.getSystemWindowInsetRight();
+ final int vertInsets = mLastInsets.getSystemWindowInsetTop()
+ + mLastInsets.getSystemWindowInsetBottom();
+
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ widthSize - horizInsets, widthMode);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ heightSize - vertInsets, heightMode);
+ }
+
+ final Behavior b = lp.getBehavior();
+ if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
+ childHeightMeasureSpec, 0)) {
+ onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
+ childHeightMeasureSpec, 0);
+ }
+
+ widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
+ lp.leftMargin + lp.rightMargin);
+
+ heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
+ lp.topMargin + lp.bottomMargin);
+ childState = View.combineMeasuredStates(childState, child.getMeasuredState());
+ }
+
+ final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
+ childState & View.MEASURED_STATE_MASK);
+ final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
+ childState << View.MEASURED_HEIGHT_STATE_SHIFT);
+ setMeasuredDimension(width, height);
+ }
+
+ private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
+ if (insets.isConsumed()) {
+ return insets;
+ }
+
+ for (int i = 0, z = getChildCount(); i < z; i++) {
+ final View child = getChildAt(i);
+ if (ViewCompat.getFitsSystemWindows(child)) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+
+ if (b != null) {
+ // If the view has a behavior, let it try first
+ insets = b.onApplyWindowInsets(this, child, insets);
+ if (insets.isConsumed()) {
+ // If it consumed the insets, break
+ break;
+ }
+ }
+ }
+ }
+
+ return insets;
+ }
+
+ /**
+ * Called to lay out each individual child view unless a
+ * {@link Behavior Behavior} is present. The Behavior may choose to
+ * delegate child measurement to this method.
+ *
+ * @param child child view to lay out
+ * @param layoutDirection the resolved layout direction for the CoordinatorLayout, such as
+ * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or
+ * {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
+ */
+ public void onLayoutChild(View child, int layoutDirection) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.checkAnchorChanged()) {
+ throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
+ + " measurement begins before layout is complete.");
+ }
+ if (lp.mAnchorView != null) {
+ layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
+ } else if (lp.keyline >= 0) {
+ layoutChildWithKeyline(child, lp.keyline, layoutDirection);
+ } else {
+ layoutChild(child, layoutDirection);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int layoutDirection = ViewCompat.getLayoutDirection(this);
+ final int childCount = mDependencySortedChildren.size();
+ for (int i = 0; i < childCount; i++) {
+ final View child = mDependencySortedChildren.get(i);
+ if (child.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior behavior = lp.getBehavior();
+
+ if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
+ onLayoutChild(child, layoutDirection);
+ }
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ super.onDraw(c);
+ if (mDrawStatusBarBackground && mStatusBarBackground != null) {
+ final int inset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
+ if (inset > 0) {
+ mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
+ mStatusBarBackground.draw(c);
+ }
+ }
+ }
+
+ @Override
+ public void setFitsSystemWindows(boolean fitSystemWindows) {
+ super.setFitsSystemWindows(fitSystemWindows);
+ setupForInsets();
+ }
+
+ /**
+ * Mark the last known child position rect for the given child view.
+ * This will be used when checking if a child view's position has changed between frames.
+ * The rect used here should be one returned by
+ * {@link #getChildRect(View, boolean, Rect)}, with translation
+ * disabled.
+ *
+ * @param child child view to set for
+ * @param r rect to set
+ */
+ void recordLastChildRect(View child, Rect r) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.setLastChildRect(r);
+ }
+
+ /**
+ * Get the last known child rect recorded by
+ * {@link #recordLastChildRect(View, Rect)}.
+ *
+ * @param child child view to retrieve from
+ * @param out rect to set to the outpur values
+ */
+ void getLastChildRect(View child, Rect out) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ out.set(lp.getLastChildRect());
+ }
+
+ /**
+ * Get the position rect for the given child. If the child has currently requested layout
+ * or has a visibility of GONE.
+ *
+ * @param child child view to check
+ * @param transform true to include transformation in the output rect, false to
+ * only account for the base position
+ * @param out rect to set to the output values
+ */
+ void getChildRect(View child, boolean transform, Rect out) {
+ if (child.isLayoutRequested() || child.getVisibility() == View.GONE) {
+ out.setEmpty();
+ return;
+ }
+ if (transform) {
+ getDescendantRect(child, out);
+ } else {
+ out.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+ }
+ }
+
+ private void getDesiredAnchoredChildRectWithoutConstraints(View child, int layoutDirection,
+ Rect anchorRect, Rect out, LayoutParams lp, int childWidth, int childHeight) {
+ final int absGravity = GravityCompat.getAbsoluteGravity(
+ resolveAnchoredChildGravity(lp.gravity), layoutDirection);
+ final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
+ resolveGravity(lp.anchorGravity),
+ layoutDirection);
+
+ final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int anchorHgrav = absAnchorGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int anchorVgrav = absAnchorGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ int left;
+ int top;
+
+ // Align to the anchor. This puts us in an assumed right/bottom child view gravity.
+ // If this is not the case we will subtract out the appropriate portion of
+ // the child size below.
+ switch (anchorHgrav) {
+ default:
+ case Gravity.LEFT:
+ left = anchorRect.left;
+ break;
+ case Gravity.RIGHT:
+ left = anchorRect.right;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ left = anchorRect.left + anchorRect.width() / 2;
+ break;
+ }
+
+ switch (anchorVgrav) {
+ default:
+ case Gravity.TOP:
+ top = anchorRect.top;
+ break;
+ case Gravity.BOTTOM:
+ top = anchorRect.bottom;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ top = anchorRect.top + anchorRect.height() / 2;
+ break;
+ }
+
+ // Offset by the child view's gravity itself. The above assumed right/bottom gravity.
+ switch (hgrav) {
+ default:
+ case Gravity.LEFT:
+ left -= childWidth;
+ break;
+ case Gravity.RIGHT:
+ // Do nothing, we're already in position.
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ left -= childWidth / 2;
+ break;
+ }
+
+ switch (vgrav) {
+ default:
+ case Gravity.TOP:
+ top -= childHeight;
+ break;
+ case Gravity.BOTTOM:
+ // Do nothing, we're already in position.
+ break;
+ case Gravity.CENTER_VERTICAL:
+ top -= childHeight / 2;
+ break;
+ }
+
+ out.set(left, top, left + childWidth, top + childHeight);
+ }
+
+ private void constrainChildRect(LayoutParams lp, Rect out, int childWidth, int childHeight) {
+ final int width = getWidth();
+ final int height = getHeight();
+
+ // Obey margins and padding
+ int left = Math.max(getPaddingLeft() + lp.leftMargin,
+ Math.min(out.left,
+ width - getPaddingRight() - childWidth - lp.rightMargin));
+ int top = Math.max(getPaddingTop() + lp.topMargin,
+ Math.min(out.top,
+ height - getPaddingBottom() - childHeight - lp.bottomMargin));
+
+ out.set(left, top, left + childWidth, top + childHeight);
+ }
+
+ /**
+ * Calculate the desired child rect relative to an anchor rect, respecting both
+ * gravity and anchorGravity.
+ *
+ * @param child child view to calculate a rect for
+ * @param layoutDirection the desired layout direction for the CoordinatorLayout
+ * @param anchorRect rect in CoordinatorLayout coordinates of the anchor view area
+ * @param out rect to set to the output values
+ */
+ void getDesiredAnchoredChildRect(View child, int layoutDirection, Rect anchorRect, Rect out) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+ getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect, out, lp,
+ childWidth, childHeight);
+ constrainChildRect(lp, out, childWidth, childHeight);
+ }
+
+ /**
+ * CORE ASSUMPTION: anchor has been laid out by the time this is called for a given child view.
+ *
+ * @param child child to lay out
+ * @param anchor view to anchor child relative to; already laid out.
+ * @param layoutDirection ViewCompat constant for layout direction
+ */
+ private void layoutChildWithAnchor(View child, View anchor, int layoutDirection) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final Rect anchorRect = acquireTempRect();
+ final Rect childRect = acquireTempRect();
+ try {
+ getDescendantRect(anchor, anchorRect);
+ getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, childRect);
+ child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
+ } finally {
+ releaseTempRect(anchorRect);
+ releaseTempRect(childRect);
+ }
+ }
+
+ /**
+ * Lay out a child view with respect to a keyline.
+ *
+ * <p>The keyline represents a horizontal offset from the unpadded starting edge of
+ * the CoordinatorLayout. The child's gravity will affect how it is positioned with
+ * respect to the keyline.</p>
+ *
+ * @param child child to lay out
+ * @param keyline offset from the starting edge in pixels of the keyline to align with
+ * @param layoutDirection ViewCompat constant for layout direction
+ */
+ private void layoutChildWithKeyline(View child, int keyline, int layoutDirection) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int absGravity = GravityCompat.getAbsoluteGravity(
+ resolveKeylineGravity(lp.gravity), layoutDirection);
+
+ final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int width = getWidth();
+ final int height = getHeight();
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ keyline = width - keyline;
+ }
+
+ int left = getKeyline(keyline) - childWidth;
+ int top = 0;
+
+ switch (hgrav) {
+ default:
+ case Gravity.LEFT:
+ // Nothing to do.
+ break;
+ case Gravity.RIGHT:
+ left += childWidth;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ left += childWidth / 2;
+ break;
+ }
+
+ switch (vgrav) {
+ default:
+ case Gravity.TOP:
+ // Do nothing, we're already in position.
+ break;
+ case Gravity.BOTTOM:
+ top += childHeight;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ top += childHeight / 2;
+ break;
+ }
+
+ // Obey margins and padding
+ left = Math.max(getPaddingLeft() + lp.leftMargin,
+ Math.min(left,
+ width - getPaddingRight() - childWidth - lp.rightMargin));
+ top = Math.max(getPaddingTop() + lp.topMargin,
+ Math.min(top,
+ height - getPaddingBottom() - childHeight - lp.bottomMargin));
+
+ child.layout(left, top, left + childWidth, top + childHeight);
+ }
+
+ /**
+ * Lay out a child view with no special handling. This will position the child as
+ * if it were within a FrameLayout or similar simple frame.
+ *
+ * @param child child view to lay out
+ * @param layoutDirection ViewCompat constant for the desired layout direction
+ */
+ private void layoutChild(View child, int layoutDirection) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Rect parent = acquireTempRect();
+ parent.set(getPaddingLeft() + lp.leftMargin,
+ getPaddingTop() + lp.topMargin,
+ getWidth() - getPaddingRight() - lp.rightMargin,
+ getHeight() - getPaddingBottom() - lp.bottomMargin);
+
+ if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
+ && !ViewCompat.getFitsSystemWindows(child)) {
+ // If we're set to handle insets but this child isn't, then it has been measured as
+ // if there are no insets. We need to lay it out to match.
+ parent.left += mLastInsets.getSystemWindowInsetLeft();
+ parent.top += mLastInsets.getSystemWindowInsetTop();
+ parent.right -= mLastInsets.getSystemWindowInsetRight();
+ parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
+ }
+
+ final Rect out = acquireTempRect();
+ GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
+ child.getMeasuredHeight(), parent, out, layoutDirection);
+ child.layout(out.left, out.top, out.right, out.bottom);
+
+ releaseTempRect(parent);
+ releaseTempRect(out);
+ }
+
+ /**
+ * Return the given gravity value, but if either or both of the axes doesn't have any gravity
+ * specified, the default value (start or top) is specified. This should be used for children
+ * that are not anchored to another view or a keyline.
+ */
+ private static int resolveGravity(int gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
+ gravity |= GravityCompat.START;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
+ gravity |= Gravity.TOP;
+ }
+ return gravity;
+ }
+
+ /**
+ * Return the given gravity value or the default if the passed value is NO_GRAVITY.
+ * This should be used for children that are positioned relative to a keyline.
+ */
+ private static int resolveKeylineGravity(int gravity) {
+ return gravity == Gravity.NO_GRAVITY ? GravityCompat.END | Gravity.TOP : gravity;
+ }
+
+ /**
+ * Return the given gravity value or the default if the passed value is NO_GRAVITY.
+ * This should be used for children that are anchored to another view.
+ */
+ private static int resolveAnchoredChildGravity(int gravity) {
+ return gravity == Gravity.NO_GRAVITY ? Gravity.CENTER : gravity;
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mBehavior != null) {
+ final float scrimAlpha = lp.mBehavior.getScrimOpacity(this, child);
+ if (scrimAlpha > 0f) {
+ if (mScrimPaint == null) {
+ mScrimPaint = new Paint();
+ }
+ mScrimPaint.setColor(lp.mBehavior.getScrimColor(this, child));
+ mScrimPaint.setAlpha(MathUtils.clamp(Math.round(255 * scrimAlpha), 0, 255));
+
+ final int saved = canvas.save();
+ if (child.isOpaque()) {
+ // If the child is opaque, there is no need to draw behind it so we'll inverse
+ // clip the canvas
+ canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(),
+ child.getBottom(), Region.Op.DIFFERENCE);
+ }
+ // Now draw the rectangle for the scrim
+ canvas.drawRect(getPaddingLeft(), getPaddingTop(),
+ getWidth() - getPaddingRight(), getHeight() - getPaddingBottom(),
+ mScrimPaint);
+ canvas.restoreToCount(saved);
+ }
+ }
+ return super.drawChild(canvas, child, drawingTime);
+ }
+
+ /**
+ * Dispatch any dependent view changes to the relevant {@link Behavior} instances.
+ *
+ * Usually run as part of the pre-draw step when at least one child view has a reported
+ * dependency on another view. This allows CoordinatorLayout to account for layout
+ * changes and animations that occur outside of the normal layout pass.
+ *
+ * It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting
+ * is completed within the correct coordinate window.
+ *
+ * The offsetting behavior implemented here does not store the computed offset in
+ * the LayoutParams; instead it expects that the layout process will always reconstruct
+ * the proper positioning.
+ *
+ * @param type the type of event which has caused this call
+ */
+ final void onChildViewsChanged(@DispatchChangeEvent final int type) {
+ final int layoutDirection = ViewCompat.getLayoutDirection(this);
+ final int childCount = mDependencySortedChildren.size();
+ final Rect inset = acquireTempRect();
+ final Rect drawRect = acquireTempRect();
+ final Rect lastDrawRect = acquireTempRect();
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = mDependencySortedChildren.get(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
+ // Do not try to update GONE child views in pre draw updates.
+ continue;
+ }
+
+ // Check child views before for anchor
+ for (int j = 0; j < i; j++) {
+ final View checkChild = mDependencySortedChildren.get(j);
+
+ if (lp.mAnchorDirectChild == checkChild) {
+ offsetChildToAnchor(child, layoutDirection);
+ }
+ }
+
+ // Get the current draw rect of the view
+ getChildRect(child, true, drawRect);
+
+ // Accumulate inset sizes
+ if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
+ final int absInsetEdge = GravityCompat.getAbsoluteGravity(
+ lp.insetEdge, layoutDirection);
+ switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
+ case Gravity.TOP:
+ inset.top = Math.max(inset.top, drawRect.bottom);
+ break;
+ case Gravity.BOTTOM:
+ inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
+ break;
+ }
+ switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ inset.left = Math.max(inset.left, drawRect.right);
+ break;
+ case Gravity.RIGHT:
+ inset.right = Math.max(inset.right, getWidth() - drawRect.left);
+ break;
+ }
+ }
+
+ // Dodge inset edges if necessary
+ if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
+ offsetChildByInset(child, inset, layoutDirection);
+ }
+
+ if (type != EVENT_VIEW_REMOVED) {
+ // Did it change? if not continue
+ getLastChildRect(child, lastDrawRect);
+ if (lastDrawRect.equals(drawRect)) {
+ continue;
+ }
+ recordLastChildRect(child, drawRect);
+ }
+
+ // Update any behavior-dependent views for the change
+ for (int j = i + 1; j < childCount; j++) {
+ final View checkChild = mDependencySortedChildren.get(j);
+ final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
+ final Behavior b = checkLp.getBehavior();
+
+ if (b != null && b.layoutDependsOn(this, checkChild, child)) {
+ if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
+ // If this is from a pre-draw and we have already been changed
+ // from a nested scroll, skip the dispatch and reset the flag
+ checkLp.resetChangedAfterNestedScroll();
+ continue;
+ }
+
+ final boolean handled;
+ switch (type) {
+ case EVENT_VIEW_REMOVED:
+ // EVENT_VIEW_REMOVED means that we need to dispatch
+ // onDependentViewRemoved() instead
+ b.onDependentViewRemoved(this, checkChild, child);
+ handled = true;
+ break;
+ default:
+ // Otherwise we dispatch onDependentViewChanged()
+ handled = b.onDependentViewChanged(this, checkChild, child);
+ break;
+ }
+
+ if (type == EVENT_NESTED_SCROLL) {
+ // If this is from a nested scroll, set the flag so that we may skip
+ // any resulting onPreDraw dispatch (if needed)
+ checkLp.setChangedAfterNestedScroll(handled);
+ }
+ }
+ }
+ }
+
+ releaseTempRect(inset);
+ releaseTempRect(drawRect);
+ releaseTempRect(lastDrawRect);
+ }
+
+ private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) {
+ if (!ViewCompat.isLaidOut(child)) {
+ // The view has not been laid out yet, so we can't obtain its bounds.
+ return;
+ }
+
+ if (child.getWidth() <= 0 || child.getHeight() <= 0) {
+ // Bounds are empty so there is nothing to dodge against, skip...
+ return;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior behavior = lp.getBehavior();
+ final Rect dodgeRect = acquireTempRect();
+ final Rect bounds = acquireTempRect();
+ bounds.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+
+ if (behavior != null && behavior.getInsetDodgeRect(this, child, dodgeRect)) {
+ // Make sure that the rect is within the view's bounds
+ if (!bounds.contains(dodgeRect)) {
+ throw new IllegalArgumentException("Rect should be within the child's bounds."
+ + " Rect:" + dodgeRect.toShortString()
+ + " | Bounds:" + bounds.toShortString());
+ }
+ } else {
+ dodgeRect.set(bounds);
+ }
+
+ // We can release the bounds rect now
+ releaseTempRect(bounds);
+
+ if (dodgeRect.isEmpty()) {
+ // Rect is empty so there is nothing to dodge against, skip...
+ releaseTempRect(dodgeRect);
+ return;
+ }
+
+ final int absDodgeInsetEdges = GravityCompat.getAbsoluteGravity(lp.dodgeInsetEdges,
+ layoutDirection);
+
+ boolean offsetY = false;
+ if ((absDodgeInsetEdges & Gravity.TOP) == Gravity.TOP) {
+ int distance = dodgeRect.top - lp.topMargin - lp.mInsetOffsetY;
+ if (distance < inset.top) {
+ setInsetOffsetY(child, inset.top - distance);
+ offsetY = true;
+ }
+ }
+ if ((absDodgeInsetEdges & Gravity.BOTTOM) == Gravity.BOTTOM) {
+ int distance = getHeight() - dodgeRect.bottom - lp.bottomMargin + lp.mInsetOffsetY;
+ if (distance < inset.bottom) {
+ setInsetOffsetY(child, distance - inset.bottom);
+ offsetY = true;
+ }
+ }
+ if (!offsetY) {
+ setInsetOffsetY(child, 0);
+ }
+
+ boolean offsetX = false;
+ if ((absDodgeInsetEdges & Gravity.LEFT) == Gravity.LEFT) {
+ int distance = dodgeRect.left - lp.leftMargin - lp.mInsetOffsetX;
+ if (distance < inset.left) {
+ setInsetOffsetX(child, inset.left - distance);
+ offsetX = true;
+ }
+ }
+ if ((absDodgeInsetEdges & Gravity.RIGHT) == Gravity.RIGHT) {
+ int distance = getWidth() - dodgeRect.right - lp.rightMargin + lp.mInsetOffsetX;
+ if (distance < inset.right) {
+ setInsetOffsetX(child, distance - inset.right);
+ offsetX = true;
+ }
+ }
+ if (!offsetX) {
+ setInsetOffsetX(child, 0);
+ }
+
+ releaseTempRect(dodgeRect);
+ }
+
+ private void setInsetOffsetX(View child, int offsetX) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mInsetOffsetX != offsetX) {
+ final int dx = offsetX - lp.mInsetOffsetX;
+ ViewCompat.offsetLeftAndRight(child, dx);
+ lp.mInsetOffsetX = offsetX;
+ }
+ }
+
+ private void setInsetOffsetY(View child, int offsetY) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mInsetOffsetY != offsetY) {
+ final int dy = offsetY - lp.mInsetOffsetY;
+ ViewCompat.offsetTopAndBottom(child, dy);
+ lp.mInsetOffsetY = offsetY;
+ }
+ }
+
+ /**
+ * Allows the caller to manually dispatch
+ * {@link Behavior#onDependentViewChanged(CoordinatorLayout, View, View)} to the associated
+ * {@link Behavior} instances of views which depend on the provided {@link View}.
+ *
+ * <p>You should not normally need to call this method as the it will be automatically done
+ * when the view has changed.
+ *
+ * @param view the View to find dependents of to dispatch the call.
+ */
+ public void dispatchDependentViewsChanged(View view) {
+ final List<View> dependents = mChildDag.getIncomingEdges(view);
+ if (dependents != null && !dependents.isEmpty()) {
+ for (int i = 0; i < dependents.size(); i++) {
+ final View child = dependents.get(i);
+ LayoutParams lp = (LayoutParams)
+ child.getLayoutParams();
+ Behavior b = lp.getBehavior();
+ if (b != null) {
+ b.onDependentViewChanged(this, child, view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the list of views which the provided view depends on. Do not store this list as its
+ * contents may not be valid beyond the caller.
+ *
+ * @param child the view to find dependencies for.
+ *
+ * @return the list of views which {@code child} depends on.
+ */
+ @NonNull
+ public List<View> getDependencies(@NonNull View child) {
+ final List<View> dependencies = mChildDag.getOutgoingEdges(child);
+ mTempDependenciesList.clear();
+ if (dependencies != null) {
+ mTempDependenciesList.addAll(dependencies);
+ }
+ return mTempDependenciesList;
+ }
+
+ /**
+ * Returns the list of views which depend on the provided view. Do not store this list as its
+ * contents may not be valid beyond the caller.
+ *
+ * @param child the view to find dependents of.
+ *
+ * @return the list of views which depend on {@code child}.
+ */
+ @NonNull
+ public List<View> getDependents(@NonNull View child) {
+ final List<View> edges = mChildDag.getIncomingEdges(child);
+ mTempDependenciesList.clear();
+ if (edges != null) {
+ mTempDependenciesList.addAll(edges);
+ }
+ return mTempDependenciesList;
+ }
+
+ @VisibleForTesting
+ final List<View> getDependencySortedChildren() {
+ prepareChildren();
+ return Collections.unmodifiableList(mDependencySortedChildren);
+ }
+
+ /**
+ * Add or remove the pre-draw listener as necessary.
+ */
+ void ensurePreDrawListener() {
+ boolean hasDependencies = false;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (hasDependencies(child)) {
+ hasDependencies = true;
+ break;
+ }
+ }
+
+ if (hasDependencies != mNeedsPreDrawListener) {
+ if (hasDependencies) {
+ addPreDrawListener();
+ } else {
+ removePreDrawListener();
+ }
+ }
+ }
+
+ /**
+ * Check if the given child has any layout dependencies on other child views.
+ */
+ private boolean hasDependencies(View child) {
+ return mChildDag.hasOutgoingEdges(child);
+ }
+
+ /**
+ * Add the pre-draw listener if we're attached to a window and mark that we currently
+ * need it when attached.
+ */
+ void addPreDrawListener() {
+ if (mIsAttachedToWindow) {
+ // Add the listener
+ if (mOnPreDrawListener == null) {
+ mOnPreDrawListener = new OnPreDrawListener();
+ }
+ final ViewTreeObserver vto = getViewTreeObserver();
+ vto.addOnPreDrawListener(mOnPreDrawListener);
+ }
+
+ // Record that we need the listener regardless of whether or not we're attached.
+ // We'll add the real listener when we become attached.
+ mNeedsPreDrawListener = true;
+ }
+
+ /**
+ * Remove the pre-draw listener if we're attached to a window and mark that we currently
+ * do not need it when attached.
+ */
+ void removePreDrawListener() {
+ if (mIsAttachedToWindow) {
+ if (mOnPreDrawListener != null) {
+ final ViewTreeObserver vto = getViewTreeObserver();
+ vto.removeOnPreDrawListener(mOnPreDrawListener);
+ }
+ }
+ mNeedsPreDrawListener = false;
+ }
+
+ /**
+ * Adjust the child left, top, right, bottom rect to the correct anchor view position,
+ * respecting gravity and anchor gravity.
+ *
+ * Note that child translation properties are ignored in this process, allowing children
+ * to be animated away from their anchor. However, if the anchor view is animated,
+ * the child will be offset to match the anchor's translated position.
+ */
+ void offsetChildToAnchor(View child, int layoutDirection) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mAnchorView != null) {
+ final Rect anchorRect = acquireTempRect();
+ final Rect childRect = acquireTempRect();
+ final Rect desiredChildRect = acquireTempRect();
+
+ getDescendantRect(lp.mAnchorView, anchorRect);
+ getChildRect(child, false, childRect);
+
+ int childWidth = child.getMeasuredWidth();
+ int childHeight = child.getMeasuredHeight();
+ getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
+ desiredChildRect, lp, childWidth, childHeight);
+ boolean changed = desiredChildRect.left != childRect.left ||
+ desiredChildRect.top != childRect.top;
+ constrainChildRect(lp, desiredChildRect, childWidth, childHeight);
+
+ final int dx = desiredChildRect.left - childRect.left;
+ final int dy = desiredChildRect.top - childRect.top;
+
+ if (dx != 0) {
+ ViewCompat.offsetLeftAndRight(child, dx);
+ }
+ if (dy != 0) {
+ ViewCompat.offsetTopAndBottom(child, dy);
+ }
+
+ if (changed) {
+ // If we have needed to move, make sure to notify the child's Behavior
+ final Behavior b = lp.getBehavior();
+ if (b != null) {
+ b.onDependentViewChanged(this, child, lp.mAnchorView);
+ }
+ }
+
+ releaseTempRect(anchorRect);
+ releaseTempRect(childRect);
+ releaseTempRect(desiredChildRect);
+ }
+ }
+
+ /**
+ * Check if a given point in the CoordinatorLayout's coordinates are within the view bounds
+ * of the given direct child view.
+ *
+ * @param child child view to test
+ * @param x X coordinate to test, in the CoordinatorLayout's coordinate system
+ * @param y Y coordinate to test, in the CoordinatorLayout's coordinate system
+ * @return true if the point is within the child view's bounds, false otherwise
+ */
+ public boolean isPointInChildBounds(View child, int x, int y) {
+ final Rect r = acquireTempRect();
+ getDescendantRect(child, r);
+ try {
+ return r.contains(x, y);
+ } finally {
+ releaseTempRect(r);
+ }
+ }
+
+ /**
+ * Check whether two views overlap each other. The views need to be descendants of this
+ * {@link CoordinatorLayout} in the view hierarchy.
+ *
+ * @param first first child view to test
+ * @param second second child view to test
+ * @return true if both views are visible and overlap each other
+ */
+ public boolean doViewsOverlap(View first, View second) {
+ if (first.getVisibility() == VISIBLE && second.getVisibility() == VISIBLE) {
+ final Rect firstRect = acquireTempRect();
+ getChildRect(first, first.getParent() != this, firstRect);
+ final Rect secondRect = acquireTempRect();
+ getChildRect(second, second.getParent() != this, secondRect);
+ try {
+ return !(firstRect.left > secondRect.right || firstRect.top > secondRect.bottom
+ || firstRect.right < secondRect.left || firstRect.bottom < secondRect.top);
+ } finally {
+ releaseTempRect(firstRect);
+ releaseTempRect(secondRect);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) p);
+ } else if (p instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) p);
+ }
+ return new LayoutParams(p);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int axes, int type) {
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ if (view.getVisibility() == View.GONE) {
+ // If it's GONE, don't dispatch
+ continue;
+ }
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
+ target, axes, type);
+ handled |= accepted;
+ lp.setNestedScrollAccepted(type, accepted);
+ } else {
+ lp.setNestedScrollAccepted(type, false);
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
+ onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
+ mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
+ mNestedScrollingTarget = target;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(type)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onNestedScrollAccepted(this, view, child, target,
+ nestedScrollAxes, type);
+ }
+ }
+ }
+
+ @Override
+ public void onStopNestedScroll(View target) {
+ onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
+ }
+
+ @Override
+ public void onStopNestedScroll(View target, int type) {
+ mNestedScrollingParentHelper.onStopNestedScroll(target, type);
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(type)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onStopNestedScroll(this, view, target, type);
+ }
+ lp.resetNestedScroll(type);
+ lp.resetChangedAfterNestedScroll();
+ }
+ mNestedScrollingTarget = null;
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ ViewCompat.TYPE_TOUCH);
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed, int type) {
+ final int childCount = getChildCount();
+ boolean accepted = false;
+
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ if (view.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(type)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed, type);
+ accepted = true;
+ }
+ }
+
+ if (accepted) {
+ onChildViewsChanged(EVENT_NESTED_SCROLL);
+ }
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
+ int xConsumed = 0;
+ int yConsumed = 0;
+ boolean accepted = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ if (view.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(type)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ mTempIntPair[0] = mTempIntPair[1] = 0;
+ viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
+
+ xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
+ : Math.min(xConsumed, mTempIntPair[0]);
+ yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
+ : Math.min(yConsumed, mTempIntPair[1]);
+
+ accepted = true;
+ }
+ }
+
+ consumed[0] = xConsumed;
+ consumed[1] = yConsumed;
+
+ if (accepted) {
+ onChildViewsChanged(EVENT_NESTED_SCROLL);
+ }
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ if (view.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(ViewCompat.TYPE_TOUCH)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
+ consumed);
+ }
+ }
+ if (handled) {
+ onChildViewsChanged(EVENT_NESTED_SCROLL);
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ if (view.getVisibility() == GONE) {
+ // If the child is GONE, skip...
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted(ViewCompat.TYPE_TOUCH)) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ public int getNestedScrollAxes() {
+ return mNestedScrollingParentHelper.getNestedScrollAxes();
+ }
+
+ class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
+ @Override
+ public boolean onPreDraw() {
+ onChildViewsChanged(EVENT_PRE_DRAW);
+ return true;
+ }
+ }
+
+ /**
+ * Sorts child views with higher Z values to the beginning of a collection.
+ */
+ static class ViewElevationComparator implements Comparator<View> {
+ @Override
+ public int compare(View lhs, View rhs) {
+ final float lz = ViewCompat.getZ(lhs);
+ final float rz = ViewCompat.getZ(rhs);
+ if (lz > rz) {
+ return -1;
+ } else if (lz < rz) {
+ return 1;
+ }
+ return 0;
+ }
+ }
+
+ /**
+ * Defines the default {@link Behavior} of a {@link View} class.
+ *
+ * <p>When writing a custom view, use this annotation to define the default behavior
+ * when used as a direct child of an {@link CoordinatorLayout}. The default behavior
+ * can be overridden using {@link LayoutParams#setBehavior}.</p>
+ *
+ * <p>Example: <code>@DefaultBehavior(MyBehavior.class)</code></p>
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface DefaultBehavior {
+ Class<? extends Behavior> value();
+ }
+
+ /**
+ * Interaction behavior plugin for child views of {@link CoordinatorLayout}.
+ *
+ * <p>A Behavior implements one or more interactions that a user can take on a child view.
+ * These interactions may include drags, swipes, flings, or any other gestures.</p>
+ *
+ * @param <V> The View type that this Behavior operates on
+ */
+ public static abstract class Behavior<V extends View> {
+
+ /**
+ * Default constructor for instantiating Behaviors.
+ */
+ public Behavior() {
+ }
+
+ /**
+ * Default constructor for inflating Behaviors from layout. The Behavior will have
+ * the opportunity to parse specially defined layout parameters. These parameters will
+ * appear on the child view tag.
+ *
+ * @param context
+ * @param attrs
+ */
+ public Behavior(Context context, AttributeSet attrs) {
+ }
+
+ /**
+ * Called when the Behavior has been attached to a LayoutParams instance.
+ *
+ * <p>This will be called after the LayoutParams has been instantiated and can be
+ * modified.</p>
+ *
+ * @param params the LayoutParams instance that this Behavior has been attached to
+ */
+ public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
+ }
+
+ /**
+ * Called when the Behavior has been detached from its holding LayoutParams instance.
+ *
+ * <p>This will only be called if the Behavior has been explicitly removed from the
+ * LayoutParams instance via {@link LayoutParams#setBehavior(Behavior)}. It will not be
+ * called if the associated view is removed from the CoordinatorLayout or similar.</p>
+ */
+ public void onDetachedFromLayoutParams() {
+ }
+
+ /**
+ * Respond to CoordinatorLayout touch events before they are dispatched to child views.
+ *
+ * <p>Behaviors can use this to monitor inbound touch events until one decides to
+ * intercept the rest of the event stream to take an action on its associated child view.
+ * This method will return false until it detects the proper intercept conditions, then
+ * return true once those conditions have occurred.</p>
+ *
+ * <p>Once a Behavior intercepts touch events, the rest of the event stream will
+ * be sent to the {@link #onTouchEvent} method.</p>
+ *
+ * <p>This method will be called regardless of the visibility of the associated child
+ * of the behavior. If you only wish to handle touch events when the child is visible, you
+ * should add a check to {@link View#isShown()} on the given child.</p>
+ *
+ * <p>The default implementation of this method always returns false.</p>
+ *
+ * @param parent the parent view currently receiving this touch event
+ * @param child the child view associated with this Behavior
+ * @param ev the MotionEvent describing the touch event being processed
+ * @return true if this Behavior would like to intercept and take over the event stream.
+ * The default always returns false.
+ */
+ public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
+ return false;
+ }
+
+ /**
+ * Respond to CoordinatorLayout touch events after this Behavior has started
+ * {@link #onInterceptTouchEvent intercepting} them.
+ *
+ * <p>Behaviors may intercept touch events in order to help the CoordinatorLayout
+ * manipulate its child views. For example, a Behavior may allow a user to drag a
+ * UI pane open or closed. This method should perform actual mutations of view
+ * layout state.</p>
+ *
+ * <p>This method will be called regardless of the visibility of the associated child
+ * of the behavior. If you only wish to handle touch events when the child is visible, you
+ * should add a check to {@link View#isShown()} on the given child.</p>
+ *
+ * @param parent the parent view currently receiving this touch event
+ * @param child the child view associated with this Behavior
+ * @param ev the MotionEvent describing the touch event being processed
+ * @return true if this Behavior handled this touch event and would like to continue
+ * receiving events in this stream. The default always returns false.
+ */
+ public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
+ return false;
+ }
+
+ /**
+ * Supply a scrim color that will be painted behind the associated child view.
+ *
+ * <p>A scrim may be used to indicate that the other elements beneath it are not currently
+ * interactive or actionable, drawing user focus and attention to the views above the scrim.
+ * </p>
+ *
+ * <p>The default implementation returns {@link Color#BLACK}.</p>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view above the scrim
+ * @return the desired scrim color in 0xAARRGGBB format. The default return value is
+ * {@link Color#BLACK}.
+ * @see #getScrimOpacity(CoordinatorLayout, View)
+ */
+ @ColorInt
+ public int getScrimColor(CoordinatorLayout parent, V child) {
+ return Color.BLACK;
+ }
+
+ /**
+ * Determine the current opacity of the scrim behind a given child view
+ *
+ * <p>A scrim may be used to indicate that the other elements beneath it are not currently
+ * interactive or actionable, drawing user focus and attention to the views above the scrim.
+ * </p>
+ *
+ * <p>The default implementation returns 0.0f.</p>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view above the scrim
+ * @return the desired scrim opacity from 0.0f to 1.0f. The default return value is 0.0f.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getScrimOpacity(CoordinatorLayout parent, V child) {
+ return 0.f;
+ }
+
+ /**
+ * Determine whether interaction with views behind the given child in the child order
+ * should be blocked.
+ *
+ * <p>The default implementation returns true if
+ * {@link #getScrimOpacity(CoordinatorLayout, View)} would return > 0.0f.</p>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view to test
+ * @return true if {@link #getScrimOpacity(CoordinatorLayout, View)} would
+ * return > 0.0f.
+ */
+ public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
+ return getScrimOpacity(parent, child) > 0.f;
+ }
+
+ /**
+ * Determine whether the supplied child view has another specific sibling view as a
+ * layout dependency.
+ *
+ * <p>This method will be called at least once in response to a layout request. If it
+ * returns true for a given child and dependency view pair, the parent CoordinatorLayout
+ * will:</p>
+ * <ol>
+ * <li>Always lay out this child after the dependent child is laid out, regardless
+ * of child order.</li>
+ * <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
+ * position changes.</li>
+ * </ol>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view to test
+ * @param dependency the proposed dependency of child
+ * @return true if child's layout depends on the proposed dependency's layout,
+ * false otherwise
+ *
+ * @see #onDependentViewChanged(CoordinatorLayout, View, View)
+ */
+ public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
+ return false;
+ }
+
+ /**
+ * Respond to a change in a child's dependent view
+ *
+ * <p>This method is called whenever a dependent view changes in size or position outside
+ * of the standard layout flow. A Behavior may use this method to appropriately update
+ * the child view in response.</p>
+ *
+ * <p>A view's dependency is determined by
+ * {@link #layoutDependsOn(CoordinatorLayout, View, View)} or
+ * if {@code child} has set another view as it's anchor.</p>
+ *
+ * <p>Note that if a Behavior changes the layout of a child via this method, it should
+ * also be able to reconstruct the correct position in
+ * {@link #onLayoutChild(CoordinatorLayout, View, int) onLayoutChild}.
+ * <code>onDependentViewChanged</code> will not be called during normal layout since
+ * the layout of each child view will always happen in dependency order.</p>
+ *
+ * <p>If the Behavior changes the child view's size or position, it should return true.
+ * The default implementation returns false.</p>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view to manipulate
+ * @param dependency the dependent view that changed
+ * @return true if the Behavior changed the child view's size or position, false otherwise
+ */
+ public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
+ return false;
+ }
+
+ /**
+ * Respond to a child's dependent view being removed.
+ *
+ * <p>This method is called after a dependent view has been removed from the parent.
+ * A Behavior may use this method to appropriately update the child view in response.</p>
+ *
+ * <p>A view's dependency is determined by
+ * {@link #layoutDependsOn(CoordinatorLayout, View, View)} or
+ * if {@code child} has set another view as it's anchor.</p>
+ *
+ * @param parent the parent view of the given child
+ * @param child the child view to manipulate
+ * @param dependency the dependent view that has been removed
+ */
+ public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
+ }
+
+ /**
+ * Called when the parent CoordinatorLayout is about to measure the given child view.
+ *
+ * <p>This method can be used to perform custom or modified measurement of a child view
+ * in place of the default child measurement behavior. The Behavior's implementation
+ * can delegate to the standard CoordinatorLayout measurement behavior by calling
+ * {@link CoordinatorLayout#onMeasureChild(View, int, int, int, int)
+ * parent.onMeasureChild}.</p>
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child the child to measure
+ * @param parentWidthMeasureSpec the width requirements for this view
+ * @param widthUsed extra space that has been used up by the parent
+ * horizontally (possibly by other children of the parent)
+ * @param parentHeightMeasureSpec the height requirements for this view
+ * @param heightUsed extra space that has been used up by the parent
+ * vertically (possibly by other children of the parent)
+ * @return true if the Behavior measured the child view, false if the CoordinatorLayout
+ * should perform its default measurement
+ */
+ public boolean onMeasureChild(CoordinatorLayout parent, V child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ return false;
+ }
+
+ /**
+ * Called when the parent CoordinatorLayout is about the lay out the given child view.
+ *
+ * <p>This method can be used to perform custom or modified layout of a child view
+ * in place of the default child layout behavior. The Behavior's implementation can
+ * delegate to the standard CoordinatorLayout measurement behavior by calling
+ * {@link CoordinatorLayout#onLayoutChild(View, int)
+ * parent.onLayoutChild}.</p>
+ *
+ * <p>If a Behavior implements
+ * {@link #onDependentViewChanged(CoordinatorLayout, View, View)}
+ * to change the position of a view in response to a dependent view changing, it
+ * should also implement <code>onLayoutChild</code> in such a way that respects those
+ * dependent views. <code>onLayoutChild</code> will always be called for a dependent view
+ * <em>after</em> its dependency has been laid out.</p>
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child child view to lay out
+ * @param layoutDirection the resolved layout direction for the CoordinatorLayout, such as
+ * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or
+ * {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
+ * @return true if the Behavior performed layout of the child view, false to request
+ * default layout behavior
+ */
+ public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
+ return false;
+ }
+
+ // Utility methods for accessing child-specific, behavior-modifiable properties.
+
+ /**
+ * Associate a Behavior-specific tag object with the given child view.
+ * This object will be stored with the child view's LayoutParams.
+ *
+ * @param child child view to set tag with
+ * @param tag tag object to set
+ */
+ public static void setTag(View child, Object tag) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.mBehaviorTag = tag;
+ }
+
+ /**
+ * Get the behavior-specific tag object with the given child view.
+ * This object is stored with the child view's LayoutParams.
+ *
+ * @param child child view to get tag with
+ * @return the previously stored tag object
+ */
+ public static Object getTag(View child) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ return lp.mBehaviorTag;
+ }
+
+ /**
+ * @deprecated You should now override
+ * {@link #onStartNestedScroll(CoordinatorLayout, View, View, View, int, int)}. This
+ * method will still continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
+ */
+ @Deprecated
+ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
+ @ScrollAxis int axes) {
+ return false;
+ }
+
+ /**
+ * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
+ * to this event and return true to indicate that the CoordinatorLayout should act as
+ * a nested scrolling parent for this scroll. Only Behaviors that return true from
+ * this method will receive subsequent nested scroll events.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param directTargetChild the child view of the CoordinatorLayout that either is or
+ * contains the target of the nested scroll operation
+ * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
+ * @param axes the axes that this nested scroll applies to. See
+ * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
+ * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
+ * @param type the type of input which cause this scroll event
+ * @return true if the Behavior wishes to accept this nested scroll
+ *
+ * @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
+ */
+ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
+ @ScrollAxis int axes, @NestedScrollType int type) {
+ if (type == ViewCompat.TYPE_TOUCH) {
+ return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
+ target, axes);
+ }
+ return false;
+ }
+
+ /**
+ * @deprecated You should now override
+ * {@link #onNestedScrollAccepted(CoordinatorLayout, View, View, View, int, int)}. This
+ * method will still continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
+ */
+ @Deprecated
+ public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
+ @ScrollAxis int axes) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll has been accepted by the CoordinatorLayout.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param directTargetChild the child view of the CoordinatorLayout that either is or
+ * contains the target of the nested scroll operation
+ * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
+ * @param axes the axes that this nested scroll applies to. See
+ * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
+ * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
+ * @param type the type of input which cause this scroll event
+ *
+ * @see NestedScrollingParent2#onNestedScrollAccepted(View, View, int, int)
+ */
+ public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
+ @ScrollAxis int axes, @NestedScrollType int type) {
+ if (type == ViewCompat.TYPE_TOUCH) {
+ onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
+ target, axes);
+ }
+ }
+
+ /**
+ * @deprecated You should now override
+ * {@link #onStopNestedScroll(CoordinatorLayout, View, View, int)}. This method will still
+ * continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
+ */
+ @Deprecated
+ public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll has ended.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onStopNestedScroll</code> marks the end of a single nested scroll event
+ * sequence. This is a good place to clean up any state related to the nested scroll.
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout that initiated
+ * the nested scroll
+ * @param type the type of input which cause this scroll event
+ *
+ * @see NestedScrollingParent2#onStopNestedScroll(View, int)
+ */
+ public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target, @NestedScrollType int type) {
+ if (type == ViewCompat.TYPE_TOUCH) {
+ onStopNestedScroll(coordinatorLayout, child, target);
+ }
+ }
+
+ /**
+ * @deprecated You should now override
+ * {@link #onNestedScroll(CoordinatorLayout, View, View, int, int, int, int, int)}.
+ * This method will still continue to be called if the type is
+ * {@link ViewCompat#TYPE_TOUCH}.
+ */
+ @Deprecated
+ public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
+ @NonNull View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll in progress has updated and the target has scrolled or
+ * attempted to scroll.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedScroll</code> is called each time the nested scroll is updated by the
+ * nested scrolling child, with both consumed and unconsumed components of the scroll
+ * supplied in pixels. <em>Each Behavior responding to the nested scroll will receive the
+ * same values.</em>
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param dxConsumed horizontal pixels consumed by the target's own scrolling operation
+ * @param dyConsumed vertical pixels consumed by the target's own scrolling operation
+ * @param dxUnconsumed horizontal pixels not consumed by the target's own scrolling
+ * operation, but requested by the user
+ * @param dyUnconsumed vertical pixels not consumed by the target's own scrolling operation,
+ * but requested by the user
+ * @param type the type of input which cause this scroll event
+ *
+ * @see NestedScrollingParent2#onNestedScroll(View, int, int, int, int, int)
+ */
+ public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
+ @NonNull View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
+ if (type == ViewCompat.TYPE_TOUCH) {
+ onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed);
+ }
+ }
+
+ /**
+ * @deprecated You should now override
+ * {@link #onNestedPreScroll(CoordinatorLayout, View, View, int, int, int[], int)}.
+ * This method will still continue to be called if the type is
+ * {@link ViewCompat#TYPE_TOUCH}.
+ */
+ @Deprecated
+ public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll in progress is about to update, before the target has
+ * consumed any of the scrolled distance.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedPreScroll</code> is called each time the nested scroll is updated
+ * by the nested scrolling child, before the nested scrolling child has consumed the scroll
+ * distance itself. <em>Each Behavior responding to the nested scroll will receive the
+ * same values.</em> The CoordinatorLayout will report as consumed the maximum number
+ * of pixels in either direction that any Behavior responding to the nested scroll reported
+ * as consumed.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param dx the raw horizontal number of pixels that the user attempted to scroll
+ * @param dy the raw vertical number of pixels that the user attempted to scroll
+ * @param consumed out parameter. consumed[0] should be set to the distance of dx that
+ * was consumed, consumed[1] should be set to the distance of dy that
+ * was consumed
+ * @param type the type of input which cause this scroll event
+ *
+ * @see NestedScrollingParent2#onNestedPreScroll(View, int, int, int[], int)
+ */
+ public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
+ @NestedScrollType int type) {
+ if (type == ViewCompat.TYPE_TOUCH) {
+ onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
+ }
+ }
+
+ /**
+ * Called when a nested scrolling child is starting a fling or an action that would
+ * be a fling.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedFling</code> is called when the current nested scrolling child view
+ * detects the proper conditions for a fling. It reports if the child itself consumed
+ * the fling. If it did not, the child is expected to show some sort of overscroll
+ * indication. This method should return true if it consumes the fling, so that a child
+ * that did not itself take an action in response can choose not to show an overfling
+ * indication.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param velocityX horizontal velocity of the attempted fling
+ * @param velocityY vertical velocity of the attempted fling
+ * @param consumed true if the nested child view consumed the fling
+ * @return true if the Behavior consumed the fling
+ *
+ * @see NestedScrollingParent#onNestedFling(View, float, float, boolean)
+ */
+ public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target, float velocityX, float velocityY,
+ boolean consumed) {
+ return false;
+ }
+
+ /**
+ * Called when a nested scrolling child is about to start a fling.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedPreFling</code> is called when the current nested scrolling child view
+ * detects the proper conditions for a fling, but it has not acted on it yet. A
+ * Behavior can return true to indicate that it consumed the fling. If at least one
+ * Behavior returns true, the fling should not be acted upon by the child.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param velocityX horizontal velocity of the attempted fling
+ * @param velocityY vertical velocity of the attempted fling
+ * @return true if the Behavior consumed the fling
+ *
+ * @see NestedScrollingParent#onNestedPreFling(View, float, float)
+ */
+ public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
+ @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
+ return false;
+ }
+
+ /**
+ * Called when the window insets have changed.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to handle the window inset change on behalf of it's associated view.
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param insets the new window insets.
+ *
+ * @return The insets supplied, minus any insets that were consumed
+ */
+ @NonNull
+ public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
+ V child, WindowInsetsCompat insets) {
+ return insets;
+ }
+
+ /**
+ * Called when a child of the view associated with this behavior wants a particular
+ * rectangle to be positioned onto the screen.
+ *
+ * <p>The contract for this method is the same as
+ * {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)}.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is
+ * associated with
+ * @param rectangle The rectangle which the child wishes to be on the screen
+ * in the child's coordinates
+ * @param immediate true to forbid animated or delayed scrolling, false otherwise
+ * @return true if the Behavior handled the request
+ * @see ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)
+ */
+ public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout,
+ V child, Rect rectangle, boolean immediate) {
+ return false;
+ }
+
+ /**
+ * Hook allowing a behavior to re-apply a representation of its internal state that had
+ * previously been generated by {@link #onSaveInstanceState}. This function will never
+ * be called with a null state.
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child child view to restore from
+ * @param state The frozen state that had previously been returned by
+ * {@link #onSaveInstanceState}.
+ *
+ * @see #onSaveInstanceState()
+ */
+ public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
+ // no-op
+ }
+
+ /**
+ * Hook allowing a behavior to generate a representation of its internal state
+ * that can later be used to create a new instance with that same state.
+ * This state should only contain information that is not persistent or can
+ * not be reconstructed later.
+ *
+ * <p>Behavior state is only saved when both the parent {@link CoordinatorLayout} and
+ * a view using this behavior have valid IDs set.</p>
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child child view to restore from
+ *
+ * @return Returns a Parcelable object containing the behavior's current dynamic
+ * state.
+ *
+ * @see #onRestoreInstanceState(Parcelable)
+ * @see View#onSaveInstanceState()
+ */
+ public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
+ return BaseSavedState.EMPTY_STATE;
+ }
+
+ /**
+ * Called when a view is set to dodge view insets.
+ *
+ * <p>This method allows a behavior to update the rectangle that should be dodged.
+ * The rectangle should be in the parent's coordinate system and within the child's
+ * bounds. If not, a {@link IllegalArgumentException} is thrown.</p>
+ *
+ * @param parent the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param rect the rect to update with the dodge rectangle
+ * @return true the rect was updated, false if we should use the child's bounds
+ */
+ public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
+ @NonNull Rect rect) {
+ return false;
+ }
+ }
+
+ /**
+ * Parameters describing the desired layout for a child of a {@link CoordinatorLayout}.
+ */
+ public static class LayoutParams extends MarginLayoutParams {
+ /**
+ * A {@link Behavior} that the child view should obey.
+ */
+ Behavior mBehavior;
+
+ boolean mBehaviorResolved = false;
+
+ /**
+ * A {@link Gravity} value describing how this child view should lay out.
+ * If either or both of the axes are not specified, they are treated by CoordinatorLayout
+ * as {@link Gravity#TOP} or {@link GravityCompat#START}. If an
+ * {@link #setAnchorId(int) anchor} is also specified, the gravity describes how this child
+ * view should be positioned relative to its anchored position.
+ */
+ public int gravity = Gravity.NO_GRAVITY;
+
+ /**
+ * A {@link Gravity} value describing which edge of a child view's
+ * {@link #getAnchorId() anchor} view the child should position itself relative to.
+ */
+ public int anchorGravity = Gravity.NO_GRAVITY;
+
+ /**
+ * The index of the horizontal keyline specified to the parent CoordinatorLayout that this
+ * child should align relative to. If an {@link #setAnchorId(int) anchor} is present the
+ * keyline will be ignored.
+ */
+ public int keyline = -1;
+
+ /**
+ * A {@link View#getId() view id} of a descendant view of the CoordinatorLayout that
+ * this child should position relative to.
+ */
+ int mAnchorId = View.NO_ID;
+
+ /**
+ * A {@link Gravity} value describing how this child view insets the CoordinatorLayout.
+ * Other child views which are set to dodge the same inset edges will be moved appropriately
+ * so that the views do not overlap.
+ */
+ public int insetEdge = Gravity.NO_GRAVITY;
+
+ /**
+ * A {@link Gravity} value describing how this child view dodges any inset child views in
+ * the CoordinatorLayout. Any views which are inset on the same edge as this view is set to
+ * dodge will result in this view being moved so that the views do not overlap.
+ */
+ public int dodgeInsetEdges = Gravity.NO_GRAVITY;
+
+ int mInsetOffsetX;
+ int mInsetOffsetY;
+
+ View mAnchorView;
+ View mAnchorDirectChild;
+
+ private boolean mDidBlockInteraction;
+ private boolean mDidAcceptNestedScrollTouch;
+ private boolean mDidAcceptNestedScrollNonTouch;
+ private boolean mDidChangeAfterNestedScroll;
+
+ final Rect mLastChildRect = new Rect();
+
+ Object mBehaviorTag;
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ LayoutParams(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CoordinatorLayout_Layout);
+
+ this.gravity = a.getInteger(
+ R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
+ Gravity.NO_GRAVITY);
+ mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
+ View.NO_ID);
+ this.anchorGravity = a.getInteger(
+ R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
+ Gravity.NO_GRAVITY);
+
+ this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
+ -1);
+
+ insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
+ dodgeInsetEdges = a.getInt(
+ R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
+ mBehaviorResolved = a.hasValue(
+ R.styleable.CoordinatorLayout_Layout_layout_behavior);
+ if (mBehaviorResolved) {
+ mBehavior = parseBehavior(context, attrs, a.getString(
+ R.styleable.CoordinatorLayout_Layout_layout_behavior));
+ }
+ a.recycle();
+
+ if (mBehavior != null) {
+ // If we have a Behavior, dispatch that it has been attached
+ mBehavior.onAttachedToLayoutParams(this);
+ }
+ }
+
+ public LayoutParams(LayoutParams p) {
+ super(p);
+ }
+
+ public LayoutParams(MarginLayoutParams p) {
+ super(p);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * Get the id of this view's anchor.
+ *
+ * @return A {@link View#getId() view id} or {@link View#NO_ID} if there is no anchor
+ */
+ @IdRes
+ public int getAnchorId() {
+ return mAnchorId;
+ }
+
+ /**
+ * Set the id of this view's anchor.
+ *
+ * <p>The view with this id must be a descendant of the CoordinatorLayout containing
+ * the child view this LayoutParams belongs to. It may not be the child view with
+ * this LayoutParams or a descendant of it.</p>
+ *
+ * @param id The {@link View#getId() view id} of the anchor or
+ * {@link View#NO_ID} if there is no anchor
+ */
+ public void setAnchorId(@IdRes int id) {
+ invalidateAnchor();
+ mAnchorId = id;
+ }
+
+ /**
+ * Get the behavior governing the layout and interaction of the child view within
+ * a parent CoordinatorLayout.
+ *
+ * @return The current behavior or null if no behavior is specified
+ */
+ @Nullable
+ public Behavior getBehavior() {
+ return mBehavior;
+ }
+
+ /**
+ * Set the behavior governing the layout and interaction of the child view within
+ * a parent CoordinatorLayout.
+ *
+ * <p>Setting a new behavior will remove any currently associated
+ * {@link Behavior#setTag(View, Object) Behavior tag}.</p>
+ *
+ * @param behavior The behavior to set or null for no special behavior
+ */
+ public void setBehavior(@Nullable Behavior behavior) {
+ if (mBehavior != behavior) {
+ if (mBehavior != null) {
+ // First detach any old behavior
+ mBehavior.onDetachedFromLayoutParams();
+ }
+
+ mBehavior = behavior;
+ mBehaviorTag = null;
+ mBehaviorResolved = true;
+
+ if (behavior != null) {
+ // Now dispatch that the Behavior has been attached
+ behavior.onAttachedToLayoutParams(this);
+ }
+ }
+ }
+
+ /**
+ * Set the last known position rect for this child view
+ * @param r the rect to set
+ */
+ void setLastChildRect(Rect r) {
+ mLastChildRect.set(r);
+ }
+
+ /**
+ * Get the last known position rect for this child view.
+ * Note: do not mutate the result of this call.
+ */
+ Rect getLastChildRect() {
+ return mLastChildRect;
+ }
+
+ /**
+ * Returns true if the anchor id changed to another valid view id since the anchor view
+ * was resolved.
+ */
+ boolean checkAnchorChanged() {
+ return mAnchorView == null && mAnchorId != View.NO_ID;
+ }
+
+ /**
+ * Returns true if the associated Behavior previously blocked interaction with other views
+ * below the associated child since the touch behavior tracking was last
+ * {@link #resetTouchBehaviorTracking() reset}.
+ *
+ * @see #isBlockingInteractionBelow(CoordinatorLayout, View)
+ */
+ boolean didBlockInteraction() {
+ if (mBehavior == null) {
+ mDidBlockInteraction = false;
+ }
+ return mDidBlockInteraction;
+ }
+
+ /**
+ * Check if the associated Behavior wants to block interaction below the given child
+ * view. The given child view should be the child this LayoutParams is associated with.
+ *
+ * <p>Once interaction is blocked, it will remain blocked until touch interaction tracking
+ * is {@link #resetTouchBehaviorTracking() reset}.</p>
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child the child view this LayoutParams is associated with
+ * @return true to block interaction below the given child
+ */
+ boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
+ if (mDidBlockInteraction) {
+ return true;
+ }
+
+ return mDidBlockInteraction |= mBehavior != null
+ ? mBehavior.blocksInteractionBelow(parent, child)
+ : false;
+ }
+
+ /**
+ * Reset tracking of Behavior-specific touch interactions. This includes
+ * interaction blocking.
+ *
+ * @see #isBlockingInteractionBelow(CoordinatorLayout, View)
+ * @see #didBlockInteraction()
+ */
+ void resetTouchBehaviorTracking() {
+ mDidBlockInteraction = false;
+ }
+
+ void resetNestedScroll(int type) {
+ setNestedScrollAccepted(type, false);
+ }
+
+ void setNestedScrollAccepted(int type, boolean accept) {
+ switch (type) {
+ case ViewCompat.TYPE_TOUCH:
+ mDidAcceptNestedScrollTouch = accept;
+ break;
+ case ViewCompat.TYPE_NON_TOUCH:
+ mDidAcceptNestedScrollNonTouch = accept;
+ break;
+ }
+ }
+
+ boolean isNestedScrollAccepted(int type) {
+ switch (type) {
+ case ViewCompat.TYPE_TOUCH:
+ return mDidAcceptNestedScrollTouch;
+ case ViewCompat.TYPE_NON_TOUCH:
+ return mDidAcceptNestedScrollNonTouch;
+ }
+ return false;
+ }
+
+ boolean getChangedAfterNestedScroll() {
+ return mDidChangeAfterNestedScroll;
+ }
+
+ void setChangedAfterNestedScroll(boolean changed) {
+ mDidChangeAfterNestedScroll = changed;
+ }
+
+ void resetChangedAfterNestedScroll() {
+ mDidChangeAfterNestedScroll = false;
+ }
+
+ /**
+ * Check if an associated child view depends on another child view of the CoordinatorLayout.
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child the child to check
+ * @param dependency the proposed dependency to check
+ * @return true if child depends on dependency
+ */
+ boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
+ return dependency == mAnchorDirectChild
+ || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
+ || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
+ }
+
+ /**
+ * Invalidate the cached anchor view and direct child ancestor of that anchor.
+ * The anchor will need to be
+ * {@link #findAnchorView(CoordinatorLayout, View) found} before
+ * being used again.
+ */
+ void invalidateAnchor() {
+ mAnchorView = mAnchorDirectChild = null;
+ }
+
+ /**
+ * Locate the appropriate anchor view by the current {@link #setAnchorId(int) anchor id}
+ * or return the cached anchor view if already known.
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param forChild the child this LayoutParams is associated with
+ * @return the located descendant anchor view, or null if the anchor id is
+ * {@link View#NO_ID}.
+ */
+ View findAnchorView(CoordinatorLayout parent, View forChild) {
+ if (mAnchorId == View.NO_ID) {
+ mAnchorView = mAnchorDirectChild = null;
+ return null;
+ }
+
+ if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {
+ resolveAnchorView(forChild, parent);
+ }
+ return mAnchorView;
+ }
+
+ /**
+ * Determine the anchor view for the child view this LayoutParams is assigned to.
+ * Assumes mAnchorId is valid.
+ */
+ private void resolveAnchorView(final View forChild, final CoordinatorLayout parent) {
+ mAnchorView = parent.findViewById(mAnchorId);
+ if (mAnchorView != null) {
+ if (mAnchorView == parent) {
+ if (parent.isInEditMode()) {
+ mAnchorView = mAnchorDirectChild = null;
+ return;
+ }
+ throw new IllegalStateException(
+ "View can not be anchored to the the parent CoordinatorLayout");
+ }
+
+ View directChild = mAnchorView;
+ for (ViewParent p = mAnchorView.getParent();
+ p != parent && p != null;
+ p = p.getParent()) {
+ if (p == forChild) {
+ if (parent.isInEditMode()) {
+ mAnchorView = mAnchorDirectChild = null;
+ return;
+ }
+ throw new IllegalStateException(
+ "Anchor must not be a descendant of the anchored view");
+ }
+ if (p instanceof View) {
+ directChild = (View) p;
+ }
+ }
+ mAnchorDirectChild = directChild;
+ } else {
+ if (parent.isInEditMode()) {
+ mAnchorView = mAnchorDirectChild = null;
+ return;
+ }
+ throw new IllegalStateException("Could not find CoordinatorLayout descendant view"
+ + " with id " + parent.getResources().getResourceName(mAnchorId)
+ + " to anchor view " + forChild);
+ }
+ }
+
+ /**
+ * Verify that the previously resolved anchor view is still valid - that it is still
+ * a descendant of the expected parent view, it is not the child this LayoutParams
+ * is assigned to or a descendant of it, and it has the expected id.
+ */
+ private boolean verifyAnchorView(View forChild, CoordinatorLayout parent) {
+ if (mAnchorView.getId() != mAnchorId) {
+ return false;
+ }
+
+ View directChild = mAnchorView;
+ for (ViewParent p = mAnchorView.getParent();
+ p != parent;
+ p = p.getParent()) {
+ if (p == null || p == forChild) {
+ mAnchorView = mAnchorDirectChild = null;
+ return false;
+ }
+ if (p instanceof View) {
+ directChild = (View) p;
+ }
+ }
+ mAnchorDirectChild = directChild;
+ return true;
+ }
+
+ /**
+ * Checks whether the view with this LayoutParams should dodge the specified view.
+ */
+ private boolean shouldDodge(View other, int layoutDirection) {
+ LayoutParams lp = (LayoutParams) other.getLayoutParams();
+ final int absInset = GravityCompat.getAbsoluteGravity(lp.insetEdge, layoutDirection);
+ return absInset != Gravity.NO_GRAVITY && (absInset &
+ GravityCompat.getAbsoluteGravity(dodgeInsetEdges, layoutDirection)) == absInset;
+ }
+ }
+
+ private class HierarchyChangeListener implements OnHierarchyChangeListener {
+ HierarchyChangeListener() {
+ }
+
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+ }
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ onChildViewsChanged(EVENT_VIEW_REMOVED);
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
+ }
+ }
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ final SparseArray<Parcelable> behaviorStates = ss.behaviorStates;
+
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ final View child = getChildAt(i);
+ final int childId = child.getId();
+ final LayoutParams lp = getResolvedLayoutParams(child);
+ final Behavior b = lp.getBehavior();
+
+ if (childId != NO_ID && b != null) {
+ Parcelable savedState = behaviorStates.get(childId);
+ if (savedState != null) {
+ b.onRestoreInstanceState(this, child, savedState);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+
+ final SparseArray<Parcelable> behaviorStates = new SparseArray<>();
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ final View child = getChildAt(i);
+ final int childId = child.getId();
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+
+ if (childId != NO_ID && b != null) {
+ // If the child has an ID and a Behavior, let it save some state...
+ Parcelable state = b.onSaveInstanceState(this, child);
+ if (state != null) {
+ behaviorStates.append(childId, state);
+ }
+ }
+ }
+ ss.behaviorStates = behaviorStates;
+ return ss;
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior behavior = lp.getBehavior();
+
+ if (behavior != null
+ && behavior.onRequestChildRectangleOnScreen(this, child, rectangle, immediate)) {
+ return true;
+ }
+
+ return super.requestChildRectangleOnScreen(child, rectangle, immediate);
+ }
+
+ private void setupForInsets() {
+ if (Build.VERSION.SDK_INT < 21) {
+ return;
+ }
+
+ if (ViewCompat.getFitsSystemWindows(this)) {
+ if (mApplyWindowInsetsListener == null) {
+ mApplyWindowInsetsListener =
+ new android.support.v4.view.OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(View v,
+ WindowInsetsCompat insets) {
+ return setWindowInsets(insets);
+ }
+ };
+ }
+ // First apply the insets listener
+ ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener);
+
+ // Now set the sys ui flags to enable us to lay out in the window insets
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ } else {
+ ViewCompat.setOnApplyWindowInsetsListener(this, null);
+ }
+ }
+
+ protected static class SavedState extends AbsSavedState {
+ SparseArray<Parcelable> behaviorStates;
+
+ public SavedState(Parcel source, ClassLoader loader) {
+ super(source, loader);
+
+ final int size = source.readInt();
+
+ final int[] ids = new int[size];
+ source.readIntArray(ids);
+
+ final Parcelable[] states = source.readParcelableArray(loader);
+
+ behaviorStates = new SparseArray<>(size);
+ for (int i = 0; i < size; i++) {
+ behaviorStates.append(ids[i], states[i]);
+ }
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ final int size = behaviorStates != null ? behaviorStates.size() : 0;
+ dest.writeInt(size);
+
+ final int[] ids = new int[size];
+ final Parcelable[] states = new Parcelable[size];
+
+ for (int i = 0; i < size; i++) {
+ ids[i] = behaviorStates.keyAt(i);
+ states[i] = behaviorStates.valueAt(i);
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(states, flags);
+
+ }
+
+ public static final Creator<SavedState> CREATOR =
+ new ClassLoaderCreator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new SavedState(in, loader);
+ }
+
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in, null);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/core-ui/src/main/java/android/support/v4/app/package.html b/core-ui/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/core-ui/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later. The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/core-ui/src/main/java/android/support/v4/view/package.html b/core-ui/src/main/java/android/support/v4/view/package.html
deleted file mode 100755
index d80ef70..0000000
--- a/core-ui/src/main/java/android/support/v4/view/package.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<body>
-
-Support android.util classes to assist with development of applications for
-android API level 4 or later. The main features here are a variety of classes
-for handling backwards compatibility with views (for example
-{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch
-data if available), and a new
-{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over
-to the widget package).
-
-</body>
diff --git a/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java b/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
new file mode 100644
index 0000000..83c62c0
--- /dev/null
+++ b/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.util.Pools;
+import android.support.v4.util.SimpleArrayMap;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A class which represents a simple directed acyclic graph.
+ *
+ * @param <T> Class for the data objects of this graph.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public final class DirectedAcyclicGraph<T> {
+ private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10);
+ private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
+
+ private final ArrayList<T> mSortResult = new ArrayList<>();
+ private final HashSet<T> mSortTmpMarked = new HashSet<>();
+
+ /**
+ * Add a node to the graph.
+ *
+ * <p>If the node already exists in the graph then this method is a no-op.</p>
+ *
+ * @param node the node to add
+ */
+ public void addNode(@NonNull T node) {
+ if (!mGraph.containsKey(node)) {
+ mGraph.put(node, null);
+ }
+ }
+
+ /**
+ * Returns true if the node is already present in the graph, false otherwise.
+ */
+ public boolean contains(@NonNull T node) {
+ return mGraph.containsKey(node);
+ }
+
+ /**
+ * Add an edge to the graph.
+ *
+ * <p>Both the given nodes should already have been added to the graph through
+ * {@link #addNode(Object)}.</p>
+ *
+ * @param node the parent node
+ * @param incomingEdge the node which has is an incoming edge to {@code node}
+ */
+ public void addEdge(@NonNull T node, @NonNull T incomingEdge) {
+ if (!mGraph.containsKey(node) || !mGraph.containsKey(incomingEdge)) {
+ throw new IllegalArgumentException("All nodes must be present in the graph before"
+ + " being added as an edge");
+ }
+
+ ArrayList<T> edges = mGraph.get(node);
+ if (edges == null) {
+ // If edges is null, we should try and get one from the pool and add it to the graph
+ edges = getEmptyList();
+ mGraph.put(node, edges);
+ }
+ // Finally add the edge to the list
+ edges.add(incomingEdge);
+ }
+
+ /**
+ * Get any incoming edges from the given node.
+ *
+ * @return a list containing any incoming edges, or null if there are none.
+ */
+ @Nullable
+ public List getIncomingEdges(@NonNull T node) {
+ return mGraph.get(node);
+ }
+
+ /**
+ * Get any outgoing edges for the given node (i.e. nodes which have an incoming edge
+ * from the given node).
+ *
+ * @return a list containing any outgoing edges, or null if there are none.
+ */
+ @Nullable
+ public List<T> getOutgoingEdges(@NonNull T node) {
+ ArrayList<T> result = null;
+ for (int i = 0, size = mGraph.size(); i < size; i++) {
+ ArrayList<T> edges = mGraph.valueAt(i);
+ if (edges != null && edges.contains(node)) {
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+ result.add(mGraph.keyAt(i));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Checks whether we have any outgoing edges for the given node (i.e. nodes which have
+ * an incoming edge from the given node).
+ *
+ * @return <code>true</code> if the node has any outgoing edges, <code>false</code>
+ * otherwise.
+ */
+ public boolean hasOutgoingEdges(@NonNull T node) {
+ for (int i = 0, size = mGraph.size(); i < size; i++) {
+ ArrayList<T> edges = mGraph.valueAt(i);
+ if (edges != null && edges.contains(node)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clears the internal graph, and releases resources to pools.
+ */
+ public void clear() {
+ for (int i = 0, size = mGraph.size(); i < size; i++) {
+ ArrayList<T> edges = mGraph.valueAt(i);
+ if (edges != null) {
+ poolList(edges);
+ }
+ }
+ mGraph.clear();
+ }
+
+ /**
+ * Returns a topologically sorted list of the nodes in this graph. This uses the DFS algorithm
+ * as described by Cormen et al. (2001). If this graph contains cyclic dependencies then this
+ * method will throw a {@link RuntimeException}.
+ *
+ * <p>The resulting list will be ordered such that index 0 will contain the node at the bottom
+ * of the graph. The node at the end of the list will have no dependencies on other nodes.</p>
+ */
+ @NonNull
+ public ArrayList<T> getSortedList() {
+ mSortResult.clear();
+ mSortTmpMarked.clear();
+
+ // Start a DFS from each node in the graph
+ for (int i = 0, size = mGraph.size(); i < size; i++) {
+ dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
+ }
+
+ return mSortResult;
+ }
+
+ private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
+ if (result.contains(node)) {
+ // We've already seen and added the node to the result list, skip...
+ return;
+ }
+ if (tmpMarked.contains(node)) {
+ throw new RuntimeException("This graph contains cyclic dependencies");
+ }
+ // Temporarily mark the node
+ tmpMarked.add(node);
+ // Recursively dfs all of the node's edges
+ final ArrayList<T> edges = mGraph.get(node);
+ if (edges != null) {
+ for (int i = 0, size = edges.size(); i < size; i++) {
+ dfs(edges.get(i), result, tmpMarked);
+ }
+ }
+ // Unmark the node from the temporary list
+ tmpMarked.remove(node);
+ // Finally add it to the result list
+ result.add(node);
+ }
+
+ /**
+ * Returns the size of the graph
+ */
+ int size() {
+ return mGraph.size();
+ }
+
+ @NonNull
+ private ArrayList<T> getEmptyList() {
+ ArrayList<T> list = mListPool.acquire();
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ return list;
+ }
+
+ private void poolList(@NonNull ArrayList<T> list) {
+ list.clear();
+ mListPool.release(list);
+ }
+}
diff --git a/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
new file mode 100644
index 0000000..986b4c2
--- /dev/null
+++ b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class ViewGroupUtils {
+ private static final ThreadLocal<Matrix> sMatrix = new ThreadLocal<>();
+ private static final ThreadLocal<RectF> sRectF = new ThreadLocal<>();
+
+ /**
+ * This is a port of the common
+ * {@link ViewGroup#offsetDescendantRectToMyCoords(View, Rect)}
+ * from the framework, but adapted to take transformations into account. The result
+ * will be the bounding rect of the real transformed rect.
+ *
+ * @param descendant view defining the original coordinate system of rect
+ * @param rect (in/out) the rect to offset from descendant to this view's coordinate system
+ */
+ static void offsetDescendantRect(ViewGroup parent, View descendant, Rect rect) {
+ Matrix m = sMatrix.get();
+ if (m == null) {
+ m = new Matrix();
+ sMatrix.set(m);
+ } else {
+ m.reset();
+ }
+
+ offsetDescendantMatrix(parent, descendant, m);
+
+ RectF rectF = sRectF.get();
+ if (rectF == null) {
+ rectF = new RectF();
+ sRectF.set(rectF);
+ }
+ rectF.set(rect);
+ m.mapRect(rectF);
+ rect.set((int) (rectF.left + 0.5f), (int) (rectF.top + 0.5f),
+ (int) (rectF.right + 0.5f), (int) (rectF.bottom + 0.5f));
+ }
+
+ /**
+ * Retrieve the transformed bounding rect of an arbitrary descendant view.
+ * This does not need to be a direct child.
+ *
+ * @param descendant descendant view to reference
+ * @param out rect to set to the bounds of the descendant view
+ */
+ public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
+ out.set(0, 0, descendant.getWidth(), descendant.getHeight());
+ offsetDescendantRect(parent, descendant, out);
+ }
+
+ private static void offsetDescendantMatrix(ViewParent target, View view, Matrix m) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View && parent != target) {
+ final View vp = (View) parent;
+ offsetDescendantMatrix(target, vp, m);
+ m.preTranslate(-vp.getScrollX(), -vp.getScrollY());
+ }
+
+ m.preTranslate(view.getLeft(), view.getTop());
+
+ if (!view.getMatrix().isIdentity()) {
+ m.preConcat(view.getMatrix());
+ }
+ }
+}
diff --git a/core-ui/src/main/java/android/support/v4/widget/package.html b/core-ui/src/main/java/android/support/v4/widget/package.html
deleted file mode 100755
index e2c636d..0000000
--- a/core-ui/src/main/java/android/support/v4/widget/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.widget classes to assist with development of applications for
-android API level 4 or later. This includes a complete modern implementation
-of {@link android.support.v4.widget.CursorAdapter} and related classes, which
-is needed for use with {@link android.support.v4.content.CursorLoader}.
-
-</body>
diff --git a/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
new file mode 100644
index 0000000..8355fcc
--- /dev/null
+++ b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.support.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DirectedAcyclicGraphTest {
+
+ private DirectedAcyclicGraph<TestNode> mGraph;
+
+ @Before
+ public void setup() {
+ mGraph = new DirectedAcyclicGraph<>();
+ }
+
+ @Test
+ public void test_addNode() {
+ final TestNode node = new TestNode("node");
+ mGraph.addNode(node);
+ assertEquals(1, mGraph.size());
+ assertTrue(mGraph.contains(node));
+ }
+
+ @Test
+ public void test_addNodeAgain() {
+ final TestNode node = new TestNode("node");
+ mGraph.addNode(node);
+ mGraph.addNode(node);
+
+ assertEquals(1, mGraph.size());
+ assertTrue(mGraph.contains(node));
+ }
+
+ @Test
+ public void test_addEdge() {
+ final TestNode node = new TestNode("node");
+ final TestNode edge = new TestNode("edge");
+
+ mGraph.addNode(node);
+ mGraph.addNode(edge);
+ mGraph.addEdge(node, edge);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void test_addEdgeWithNotAddedEdgeNode() {
+ final TestNode node = new TestNode("node");
+ final TestNode edge = new TestNode("edge");
+
+ // Add the node, but not the edge node
+ mGraph.addNode(node);
+
+ // Now add the link
+ mGraph.addEdge(node, edge);
+ }
+
+ @Test
+ public void test_getIncomingEdges() {
+ final TestNode node = new TestNode("node");
+ final TestNode edge = new TestNode("edge");
+ mGraph.addNode(node);
+ mGraph.addNode(edge);
+ mGraph.addEdge(node, edge);
+
+ final List<TestNode> incomingEdges = mGraph.getIncomingEdges(node);
+ assertNotNull(incomingEdges);
+ assertEquals(1, incomingEdges.size());
+ assertEquals(edge, incomingEdges.get(0));
+ }
+
+ @Test
+ public void test_getOutgoingEdges() {
+ final TestNode node = new TestNode("node");
+ final TestNode edge = new TestNode("edge");
+ mGraph.addNode(node);
+ mGraph.addNode(edge);
+ mGraph.addEdge(node, edge);
+
+ // Now assert the getOutgoingEdges returns a list which has one element (node)
+ final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge);
+ assertNotNull(outgoingEdges);
+ assertEquals(1, outgoingEdges.size());
+ assertTrue(outgoingEdges.contains(node));
+ }
+
+ @Test
+ public void test_getOutgoingEdgesMultiple() {
+ final TestNode node1 = new TestNode("1");
+ final TestNode node2 = new TestNode("2");
+ final TestNode edge = new TestNode("edge");
+ mGraph.addNode(node1);
+ mGraph.addNode(node2);
+ mGraph.addNode(edge);
+
+ mGraph.addEdge(node1, edge);
+ mGraph.addEdge(node2, edge);
+
+ // Now assert the getOutgoingEdges returns a list which has 2 elements (node1 & node2)
+ final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge);
+ assertNotNull(outgoingEdges);
+ assertEquals(2, outgoingEdges.size());
+ assertTrue(outgoingEdges.contains(node1));
+ assertTrue(outgoingEdges.contains(node2));
+ }
+
+ @Test
+ public void test_hasOutgoingEdges() {
+ final TestNode node = new TestNode("node");
+ final TestNode edge = new TestNode("edge");
+ mGraph.addNode(node);
+ mGraph.addNode(edge);
+
+ // There is no edge currently and assert that fact
+ assertFalse(mGraph.hasOutgoingEdges(edge));
+ // Now add the edge
+ mGraph.addEdge(node, edge);
+ // and assert that the methods returns true;
+ assertTrue(mGraph.hasOutgoingEdges(edge));
+ }
+
+ @Test
+ public void test_clear() {
+ final TestNode node1 = new TestNode("1");
+ final TestNode node2 = new TestNode("2");
+ final TestNode edge = new TestNode("edge");
+ mGraph.addNode(node1);
+ mGraph.addNode(node2);
+ mGraph.addNode(edge);
+
+ // Now clear the graph
+ mGraph.clear();
+
+ // Now assert the graph is empty and that contains returns false
+ assertEquals(0, mGraph.size());
+ assertFalse(mGraph.contains(node1));
+ assertFalse(mGraph.contains(node2));
+ assertFalse(mGraph.contains(edge));
+ }
+
+ @Test
+ public void test_getSortedList() {
+ final TestNode node1 = new TestNode("A");
+ final TestNode node2 = new TestNode("B");
+ final TestNode node3 = new TestNode("C");
+ final TestNode node4 = new TestNode("D");
+
+ // Now we'll add the nodes
+ mGraph.addNode(node1);
+ mGraph.addNode(node2);
+ mGraph.addNode(node3);
+ mGraph.addNode(node4);
+
+ // Now we'll add edges so that 4 <- 2, 2 <- 3, 3 <- 1 (where <- denotes a dependency)
+ mGraph.addEdge(node4, node2);
+ mGraph.addEdge(node2, node3);
+ mGraph.addEdge(node3, node1);
+
+ final List<TestNode> sorted = mGraph.getSortedList();
+ // Assert that it is the correct size
+ assertEquals(4, sorted.size());
+ // Assert that all of the nodes are present and in their sorted order
+ assertEquals(node1, sorted.get(0));
+ assertEquals(node3, sorted.get(1));
+ assertEquals(node2, sorted.get(2));
+ assertEquals(node4, sorted.get(3));
+ }
+
+ private static class TestNode {
+ private final String mLabel;
+
+ TestNode(@NonNull String label) {
+ mLabel = label;
+ }
+
+ @Override
+ public String toString() {
+ return "TestNode: " + mLabel;
+ }
+ }
+
+}
diff --git a/core-utils/Android.mk b/core-utils/Android.mk
index a6855fc..6dda862 100644
--- a/core-utils/Android.mk
+++ b/core-utils/Android.mk
@@ -27,7 +27,9 @@
LOCAL_MODULE := android-support-core-utils
LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
LOCAL_SRC_FILES := \
- $(call all-java-files-under,src/main/java)
+ $(call all-java-files-under,kitkat) \
+ $(call all-java-files-under,api21) \
+ $(call all-java-files-under,java)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_SHARED_ANDROID_LIBRARIES := \
android-support-compat \
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java b/core-utils/api21/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
rename to core-utils/api21/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
diff --git a/core-utils/build.gradle b/core-utils/build.gradle
index b384a37..64c9ff8 100644
--- a/core-utils/build.gradle
+++ b/core-utils/build.gradle
@@ -19,6 +19,14 @@
defaultConfig {
minSdkVersion 14
}
+
+ sourceSets {
+ main.java.srcDirs = [
+ 'kitkat',
+ 'api21',
+ 'java'
+ ]
+ }
}
supportLibrary {
diff --git a/core-utils/src/main/java/android/support/v4/app/AppLaunchChecker.java b/core-utils/java/android/support/v4/app/AppLaunchChecker.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/AppLaunchChecker.java
rename to core-utils/java/android/support/v4/app/AppLaunchChecker.java
diff --git a/core-utils/src/main/java/android/support/v4/app/FrameMetricsAggregator.java b/core-utils/java/android/support/v4/app/FrameMetricsAggregator.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/FrameMetricsAggregator.java
rename to core-utils/java/android/support/v4/app/FrameMetricsAggregator.java
diff --git a/core-utils/src/main/java/android/support/v4/app/NavUtils.java b/core-utils/java/android/support/v4/app/NavUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/NavUtils.java
rename to core-utils/java/android/support/v4/app/NavUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/app/TaskStackBuilder.java b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/TaskStackBuilder.java
rename to core-utils/java/android/support/v4/app/TaskStackBuilder.java
diff --git a/core-utils/src/main/java/android/support/v4/content/AsyncTaskLoader.java b/core-utils/java/android/support/v4/content/AsyncTaskLoader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/AsyncTaskLoader.java
rename to core-utils/java/android/support/v4/content/AsyncTaskLoader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/CursorLoader.java b/core-utils/java/android/support/v4/content/CursorLoader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/CursorLoader.java
rename to core-utils/java/android/support/v4/content/CursorLoader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/FileProvider.java b/core-utils/java/android/support/v4/content/FileProvider.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/FileProvider.java
rename to core-utils/java/android/support/v4/content/FileProvider.java
diff --git a/core-utils/src/main/java/android/support/v4/content/Loader.java b/core-utils/java/android/support/v4/content/Loader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/Loader.java
rename to core-utils/java/android/support/v4/content/Loader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/LocalBroadcastManager.java b/core-utils/java/android/support/v4/content/LocalBroadcastManager.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/LocalBroadcastManager.java
rename to core-utils/java/android/support/v4/content/LocalBroadcastManager.java
diff --git a/core-utils/src/main/java/android/support/v4/content/MimeTypeFilter.java b/core-utils/java/android/support/v4/content/MimeTypeFilter.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/MimeTypeFilter.java
rename to core-utils/java/android/support/v4/content/MimeTypeFilter.java
diff --git a/core-utils/src/main/java/android/support/v4/content/ModernAsyncTask.java b/core-utils/java/android/support/v4/content/ModernAsyncTask.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/ModernAsyncTask.java
rename to core-utils/java/android/support/v4/content/ModernAsyncTask.java
diff --git a/core-utils/src/main/java/android/support/v4/content/PermissionChecker.java b/core-utils/java/android/support/v4/content/PermissionChecker.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/PermissionChecker.java
rename to core-utils/java/android/support/v4/content/PermissionChecker.java
diff --git a/core-utils/src/main/java/android/support/v4/content/WakefulBroadcastReceiver.java b/core-utils/java/android/support/v4/content/WakefulBroadcastReceiver.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/WakefulBroadcastReceiver.java
rename to core-utils/java/android/support/v4/content/WakefulBroadcastReceiver.java
diff --git a/core-utils/src/main/java/android/support/v4/graphics/ColorUtils.java b/core-utils/java/android/support/v4/graphics/ColorUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/ColorUtils.java
rename to core-utils/java/android/support/v4/graphics/ColorUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
rename to core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java b/core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
rename to core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
diff --git a/core-utils/src/main/java/android/support/v4/math/MathUtils.java b/core-utils/java/android/support/v4/math/MathUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/math/MathUtils.java
rename to core-utils/java/android/support/v4/math/MathUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/print/PrintHelper.java b/core-utils/java/android/support/v4/print/PrintHelper.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/print/PrintHelper.java
rename to core-utils/java/android/support/v4/print/PrintHelper.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/DocumentFile.java b/core-utils/java/android/support/v4/provider/DocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/DocumentFile.java
rename to core-utils/java/android/support/v4/provider/DocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/RawDocumentFile.java b/core-utils/java/android/support/v4/provider/RawDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/RawDocumentFile.java
rename to core-utils/java/android/support/v4/provider/RawDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/SingleDocumentFile.java b/core-utils/java/android/support/v4/provider/SingleDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/SingleDocumentFile.java
rename to core-utils/java/android/support/v4/provider/SingleDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/TreeDocumentFile.java b/core-utils/java/android/support/v4/provider/TreeDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/TreeDocumentFile.java
rename to core-utils/java/android/support/v4/provider/TreeDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/DocumentsContractApi19.java b/core-utils/kitkat/android/support/v4/provider/DocumentsContractApi19.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/DocumentsContractApi19.java
rename to core-utils/kitkat/android/support/v4/provider/DocumentsContractApi19.java
diff --git a/core-utils/src/main/java/android/support/v4/app/package.html b/core-utils/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/core-utils/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later. The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/core-utils/src/main/java/android/support/v4/content/package.html b/core-utils/src/main/java/android/support/v4/content/package.html
deleted file mode 100755
index 33bf4b5..0000000
--- a/core-utils/src/main/java/android/support/v4/content/package.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<body>
-
-Support android.content classes to assist with development of applications for
-android API level 4 or later. The main features here are
-{@link android.support.v4.content.Loader} and related classes and
-{@link android.support.v4.content.LocalBroadcastManager} to
-provide a cleaner implementation of broadcasts that don't need to go outside
-of an app.
-
-</body>
diff --git a/design/Android.mk b/design/Android.mk
index 08f8815..4a51f77 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -38,7 +38,11 @@
android-support-transition \
android-support-v7-appcompat \
android-support-v7-recyclerview \
- android-support-v4 \
+ android-support-compat \
+ android-support-media-compat \
+ android-support-core-utils \
+ android-support-core-ui \
+ android-support-fragment \
android-support-annotations
LOCAL_JAVA_LANGUAGE_VERSION := 1.7
LOCAL_AAPT_FLAGS := \
diff --git a/design/api/27.0.0.ignore b/design/api/27.0.0.ignore
new file mode 100644
index 0000000..533cc49
--- /dev/null
+++ b/design/api/27.0.0.ignore
@@ -0,0 +1,5 @@
+197ce1d
+88bc57e
+9761c3e
+86b38bf
+c6abd5e
diff --git a/design/api/current.txt b/design/api/current.txt
index 602ee48..14c1c20 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -244,96 +244,6 @@
field public static final int COLLAPSE_MODE_PIN = 1; // 0x1
}
- public class CoordinatorLayout extends android.view.ViewGroup {
- ctor public CoordinatorLayout(android.content.Context);
- ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet);
- ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet, int);
- method public void dispatchDependentViewsChanged(android.view.View);
- method public boolean doViewsOverlap(android.view.View, android.view.View);
- method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateDefaultLayoutParams();
- method public android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.util.AttributeSet);
- method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
- method public java.util.List<android.view.View> getDependencies(android.view.View);
- method public java.util.List<android.view.View> getDependents(android.view.View);
- method public android.graphics.drawable.Drawable getStatusBarBackground();
- method public boolean isPointInChildBounds(android.view.View, int, int);
- method public void onAttachedToWindow();
- method public void onDetachedFromWindow();
- method public void onDraw(android.graphics.Canvas);
- method protected void onLayout(boolean, int, int, int, int);
- method public void onLayoutChild(android.view.View, int);
- method public void onMeasureChild(android.view.View, int, int, int, int);
- method public void onNestedPreScroll(android.view.View, int, int, int[], int);
- method public void onNestedScroll(android.view.View, int, int, int, int, int);
- method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
- method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
- method public void onStopNestedScroll(android.view.View, int);
- method public void setStatusBarBackground(android.graphics.drawable.Drawable);
- method public void setStatusBarBackgroundColor(int);
- method public void setStatusBarBackgroundResource(int);
- }
-
- public static abstract class CoordinatorLayout.Behavior<V extends android.view.View> {
- ctor public CoordinatorLayout.Behavior();
- ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet);
- method public boolean blocksInteractionBelow(android.support.design.widget.CoordinatorLayout, V);
- method public boolean getInsetDodgeRect(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect);
- method public int getScrimColor(android.support.design.widget.CoordinatorLayout, V);
- method public float getScrimOpacity(android.support.design.widget.CoordinatorLayout, V);
- method public static java.lang.Object getTag(android.view.View);
- method public boolean layoutDependsOn(android.support.design.widget.CoordinatorLayout, V, android.view.View);
- method public android.support.v4.view.WindowInsetsCompat onApplyWindowInsets(android.support.design.widget.CoordinatorLayout, V, android.support.v4.view.WindowInsetsCompat);
- method public void onAttachedToLayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
- method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, V, android.view.View);
- method public void onDependentViewRemoved(android.support.design.widget.CoordinatorLayout, V, android.view.View);
- method public void onDetachedFromLayoutParams();
- method public boolean onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
- method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, V, int);
- method public boolean onMeasureChild(android.support.design.widget.CoordinatorLayout, V, int, int, int, int);
- method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
- method public boolean onNestedPreFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float);
- method public deprecated void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
- method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
- method public deprecated void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
- method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
- method public deprecated void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
- method public void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
- method public boolean onRequestChildRectangleOnScreen(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
- method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, V, android.os.Parcelable);
- method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, V);
- method public deprecated boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
- method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
- method public deprecated void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View);
- method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int);
- method public boolean onTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
- method public static void setTag(android.view.View, java.lang.Object);
- }
-
- public static abstract class CoordinatorLayout.DefaultBehavior implements java.lang.annotation.Annotation {
- }
-
- public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
- ctor public CoordinatorLayout.LayoutParams(int, int);
- ctor public CoordinatorLayout.LayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
- ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
- ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
- method public int getAnchorId();
- method public android.support.design.widget.CoordinatorLayout.Behavior getBehavior();
- method public void setAnchorId(int);
- method public void setBehavior(android.support.design.widget.CoordinatorLayout.Behavior);
- field public int anchorGravity;
- field public int dodgeInsetEdges;
- field public int gravity;
- field public int insetEdge;
- field public int keyline;
- }
-
- protected static class CoordinatorLayout.SavedState extends android.support.v4.view.AbsSavedState {
- ctor public CoordinatorLayout.SavedState(android.os.Parcel, java.lang.ClassLoader);
- ctor public CoordinatorLayout.SavedState(android.os.Parcelable);
- field public static final android.os.Parcelable.Creator<android.support.design.widget.CoordinatorLayout.SavedState> CREATOR;
- }
-
public class FloatingActionButton extends android.support.design.widget.VisibilityAwareImageButton {
ctor public FloatingActionButton(android.content.Context);
ctor public FloatingActionButton(android.content.Context, android.util.AttributeSet);
@@ -341,6 +251,7 @@
method public float getCompatElevation();
method public android.graphics.drawable.Drawable getContentBackground();
method public boolean getContentRect(android.graphics.Rect);
+ method public int getCustomSize();
method public int getRippleColor();
method public int getSize();
method public boolean getUseCompatPadding();
@@ -348,11 +259,13 @@
method public void hide(android.support.design.widget.FloatingActionButton.OnVisibilityChangedListener);
method public void setBackgroundDrawable(android.graphics.drawable.Drawable);
method public void setCompatElevation(float);
+ method public void setCustomSize(int);
method public void setRippleColor(int);
method public void setSize(int);
method public void setUseCompatPadding(boolean);
method public void show();
method public void show(android.support.design.widget.FloatingActionButton.OnVisibilityChangedListener);
+ field public static final int NO_CUSTOM_SIZE = 0; // 0x0
field public static final int SIZE_AUTO = -1; // 0xffffffff
field public static final int SIZE_MINI = 1; // 0x1
field public static final int SIZE_NORMAL = 0; // 0x0
diff --git a/design/build.gradle b/design/build.gradle
index 3af966d..06a5a55 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -17,11 +17,6 @@
androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
androidTestImplementation project(':support-testutils')
-
- testImplementation libs.junit
- testImplementation (libs.test_runner) {
- exclude module: 'support-annotations'
- }
}
android {
@@ -44,8 +39,6 @@
'res-public'
]
main.resources.srcDir 'src'
-
- test.java.srcDir 'jvm-tests/src'
}
buildTypes.all {
diff --git a/design/jvm-tests/NO_DOCS b/design/jvm-tests/NO_DOCS
deleted file mode 100644
index 092a39c..0000000
--- a/design/jvm-tests/NO_DOCS
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (C) 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-Having this file, named NO_DOCS, in a directory will prevent
-Android javadocs from being generated for java files under
-the directory. This is especially useful for test projects.
diff --git a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java b/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
deleted file mode 100644
index 4a5ffc5..0000000
--- a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.List;
-
-@RunWith(JUnit4.class)
-@SmallTest
-public class DirectedAcyclicGraphTest {
-
- private DirectedAcyclicGraph<TestNode> mGraph;
-
- @Before
- public void setup() {
- mGraph = new DirectedAcyclicGraph<>();
- }
-
- @Test
- public void test_addNode() {
- final TestNode node = new TestNode("node");
- mGraph.addNode(node);
- assertEquals(1, mGraph.size());
- assertTrue(mGraph.contains(node));
- }
-
- @Test
- public void test_addNodeAgain() {
- final TestNode node = new TestNode("node");
- mGraph.addNode(node);
- mGraph.addNode(node);
-
- assertEquals(1, mGraph.size());
- assertTrue(mGraph.contains(node));
- }
-
- @Test
- public void test_addEdge() {
- final TestNode node = new TestNode("node");
- final TestNode edge = new TestNode("edge");
-
- mGraph.addNode(node);
- mGraph.addNode(edge);
- mGraph.addEdge(node, edge);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void test_addEdgeWithNotAddedEdgeNode() {
- final TestNode node = new TestNode("node");
- final TestNode edge = new TestNode("edge");
-
- // Add the node, but not the edge node
- mGraph.addNode(node);
-
- // Now add the link
- mGraph.addEdge(node, edge);
- }
-
- @Test
- public void test_getIncomingEdges() {
- final TestNode node = new TestNode("node");
- final TestNode edge = new TestNode("edge");
- mGraph.addNode(node);
- mGraph.addNode(edge);
- mGraph.addEdge(node, edge);
-
- final List<TestNode> incomingEdges = mGraph.getIncomingEdges(node);
- assertNotNull(incomingEdges);
- assertEquals(1, incomingEdges.size());
- assertEquals(edge, incomingEdges.get(0));
- }
-
- @Test
- public void test_getOutgoingEdges() {
- final TestNode node = new TestNode("node");
- final TestNode edge = new TestNode("edge");
- mGraph.addNode(node);
- mGraph.addNode(edge);
- mGraph.addEdge(node, edge);
-
- // Now assert the getOutgoingEdges returns a list which has one element (node)
- final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge);
- assertNotNull(outgoingEdges);
- assertEquals(1, outgoingEdges.size());
- assertTrue(outgoingEdges.contains(node));
- }
-
- @Test
- public void test_getOutgoingEdgesMultiple() {
- final TestNode node1 = new TestNode("1");
- final TestNode node2 = new TestNode("2");
- final TestNode edge = new TestNode("edge");
- mGraph.addNode(node1);
- mGraph.addNode(node2);
- mGraph.addNode(edge);
-
- mGraph.addEdge(node1, edge);
- mGraph.addEdge(node2, edge);
-
- // Now assert the getOutgoingEdges returns a list which has 2 elements (node1 & node2)
- final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge);
- assertNotNull(outgoingEdges);
- assertEquals(2, outgoingEdges.size());
- assertTrue(outgoingEdges.contains(node1));
- assertTrue(outgoingEdges.contains(node2));
- }
-
- @Test
- public void test_hasOutgoingEdges() {
- final TestNode node = new TestNode("node");
- final TestNode edge = new TestNode("edge");
- mGraph.addNode(node);
- mGraph.addNode(edge);
-
- // There is no edge currently and assert that fact
- assertFalse(mGraph.hasOutgoingEdges(edge));
- // Now add the edge
- mGraph.addEdge(node, edge);
- // and assert that the methods returns true;
- assertTrue(mGraph.hasOutgoingEdges(edge));
- }
-
- @Test
- public void test_clear() {
- final TestNode node1 = new TestNode("1");
- final TestNode node2 = new TestNode("2");
- final TestNode edge = new TestNode("edge");
- mGraph.addNode(node1);
- mGraph.addNode(node2);
- mGraph.addNode(edge);
-
- // Now clear the graph
- mGraph.clear();
-
- // Now assert the graph is empty and that contains returns false
- assertEquals(0, mGraph.size());
- assertFalse(mGraph.contains(node1));
- assertFalse(mGraph.contains(node2));
- assertFalse(mGraph.contains(edge));
- }
-
- @Test
- public void test_getSortedList() {
- final TestNode node1 = new TestNode("A");
- final TestNode node2 = new TestNode("B");
- final TestNode node3 = new TestNode("C");
- final TestNode node4 = new TestNode("D");
-
- // Now we'll add the nodes
- mGraph.addNode(node1);
- mGraph.addNode(node2);
- mGraph.addNode(node3);
- mGraph.addNode(node4);
-
- // Now we'll add edges so that 4 <- 2, 2 <- 3, 3 <- 1 (where <- denotes a dependency)
- mGraph.addEdge(node4, node2);
- mGraph.addEdge(node2, node3);
- mGraph.addEdge(node3, node1);
-
- final List<TestNode> sorted = mGraph.getSortedList();
- // Assert that it is the correct size
- assertEquals(4, sorted.size());
- // Assert that all of the nodes are present and in their sorted order
- assertEquals(node1, sorted.get(0));
- assertEquals(node3, sorted.get(1));
- assertEquals(node2, sorted.get(2));
- assertEquals(node4, sorted.get(3));
- }
-
- private static class TestNode {
- private final String mLabel;
-
- TestNode(@NonNull String label) {
- mLabel = label;
- }
-
- @Override
- public String toString() {
- return "TestNode: " + mLabel;
- }
- }
-
-}
diff --git a/design/res-public/values/public_attrs.xml b/design/res-public/values/public_attrs.xml
index b443778..9afe981 100644
--- a/design/res-public/values/public_attrs.xml
+++ b/design/res-public/values/public_attrs.xml
@@ -51,19 +51,13 @@
<public type="attr" name="itemIconTint"/>
<public type="attr" name="itemTextAppearance"/>
<public type="attr" name="itemTextColor"/>
- <public type="attr" name="keylines"/>
- <public type="attr" name="layout_anchor"/>
- <public type="attr" name="layout_anchorGravity"/>
- <public type="attr" name="layout_behavior"/>
<public type="attr" name="layout_collapseMode"/>
<public type="attr" name="layout_collapseParallaxMultiplier"/>
- <public type="attr" name="layout_keyline"/>
<public type="attr" name="layout_scrollFlags"/>
<public type="attr" name="layout_scrollInterpolator"/>
<public type="attr" name="menu"/>
<public type="attr" name="pressedTranslationZ"/>
<public type="attr" name="rippleColor"/>
- <public type="attr" name="statusBarBackground"/>
<public type="attr" name="statusBarScrim"/>
<public type="attr" name="tabBackground"/>
<public type="attr" name="tabContentStart"/>
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index b378849..8c3536f 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -23,7 +23,7 @@
<!-- Ripple color for the FAB. -->
<attr name="rippleColor" format="color"/>
- <!-- Size for the FAB. -->
+ <!-- Size for the FAB. If fabCustomSize is set, this will be ignored. -->
<attr name="fabSize">
<!-- A size which will change based on the window size. -->
<enum name="auto" value="-1"/>
@@ -32,6 +32,8 @@
<!-- The mini sized button. -->
<enum name="mini" value="1"/>
</attr>
+ <!-- Custom size for the FAB. If this is set, fabSize will be ignored. -->
+ <attr name="fabCustomSize" format="dimension"/>
<!-- Elevation value for the FAB -->
<attr name="elevation"/>
<!-- TranslationZ value for the FAB when pressed-->
@@ -122,107 +124,6 @@
<attr name="android:layout" />
</declare-styleable>
- <declare-styleable name="CoordinatorLayout">
- <!-- A reference to an array of integers representing the
- locations of horizontal keylines in dp from the starting edge.
- Child views can refer to these keylines for alignment using
- layout_keyline="index" where index is a 0-based index into
- this array. -->
- <attr name="keylines" format="reference"/>
- <!-- Drawable to display behind the status bar when the view is set to draw behind it. -->
- <attr name="statusBarBackground" format="reference"/>
- </declare-styleable>
-
- <declare-styleable name="CoordinatorLayout_Layout">
- <attr name="android:layout_gravity"/>
- <!-- The class name of a Behavior class defining special runtime behavior
- for this child view. -->
- <attr name="layout_behavior" format="string"/>
- <!-- The id of an anchor view that this view should position relative to. -->
- <attr name="layout_anchor" format="reference"/>
- <!-- The index of a keyline this view should position relative to.
- android:layout_gravity will affect how the view aligns to the
- specified keyline. -->
- <attr name="layout_keyline" format="integer"/>
-
- <!-- Specifies how an object should position relative to an anchor, on both the X and Y axes,
- within its parent's bounds. -->
- <attr name="layout_anchorGravity">
- <!-- Push object to the top of its container, not changing its size. -->
- <flag name="top" value="0x30"/>
- <!-- Push object to the bottom of its container, not changing its size. -->
- <flag name="bottom" value="0x50"/>
- <!-- Push object to the left of its container, not changing its size. -->
- <flag name="left" value="0x03"/>
- <!-- Push object to the right of its container, not changing its size. -->
- <flag name="right" value="0x05"/>
- <!-- Place object in the vertical center of its container, not changing its size. -->
- <flag name="center_vertical" value="0x10"/>
- <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
- <flag name="fill_vertical" value="0x70"/>
- <!-- Place object in the horizontal center of its container, not changing its size. -->
- <flag name="center_horizontal" value="0x01"/>
- <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
- <flag name="fill_horizontal" value="0x07"/>
- <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
- <flag name="center" value="0x11"/>
- <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
- <flag name="fill" value="0x77"/>
- <!-- Additional option that can be set to have the top and/or bottom edges of
- the child clipped to its container's bounds.
- The clip will be based on the vertical gravity: a top gravity will clip the bottom
- edge, a bottom gravity will clip the top edge, and neither will clip both edges. -->
- <flag name="clip_vertical" value="0x80"/>
- <!-- Additional option that can be set to have the left and/or right edges of
- the child clipped to its container's bounds.
- The clip will be based on the horizontal gravity: a left gravity will clip the right
- edge, a right gravity will clip the left edge, and neither will clip both edges. -->
- <flag name="clip_horizontal" value="0x08"/>
- <!-- Push object to the beginning of its container, not changing its size. -->
- <flag name="start" value="0x00800003"/>
- <!-- Push object to the end of its container, not changing its size. -->
- <flag name="end" value="0x00800005"/>
- </attr>
-
- <!-- Specifies how this view insets the CoordinatorLayout and make some other views
- dodge it. -->
- <attr name="layout_insetEdge" format="enum">
- <!-- Don't inset. -->
- <enum name="none" value="0x0"/>
- <!-- Inset the top edge. -->
- <enum name="top" value="0x30"/>
- <!-- Inset the bottom edge. -->
- <enum name="bottom" value="0x50"/>
- <!-- Inset the left edge. -->
- <enum name="left" value="0x03"/>
- <!-- Inset the right edge. -->
- <enum name="right" value="0x03"/>
- <!-- Inset the start edge. -->
- <enum name="start" value="0x00800003"/>
- <!-- Inset the end edge. -->
- <enum name="end" value="0x00800005"/>
- </attr>
- <!-- Specifies how this view dodges the inset edges of the CoordinatorLayout. -->
- <attr name="layout_dodgeInsetEdges">
- <!-- Don't dodge any edges -->
- <flag name="none" value="0x0"/>
- <!-- Dodge the top inset edge. -->
- <flag name="top" value="0x30"/>
- <!-- Dodge the bottom inset edge. -->
- <flag name="bottom" value="0x50"/>
- <!-- Dodge the left inset edge. -->
- <flag name="left" value="0x03"/>
- <!-- Dodge the right inset edge. -->
- <flag name="right" value="0x03"/>
- <!-- Dodge the start inset edge. -->
- <flag name="start" value="0x00800003"/>
- <!-- Dodge the end inset edge. -->
- <flag name="end" value="0x00800005"/>
- <!-- Dodge all the inset edges. -->
- <flag name="all" value="0x77"/>
- </attr>
- </declare-styleable>
-
<declare-styleable name="TextInputLayout">
<attr name="hintTextAppearance" format="reference"/>
<!-- The hint to display in the floating label. -->
diff --git a/design/res/values/styles.xml b/design/res/values/styles.xml
index 93fb7eb..bbb200d 100644
--- a/design/res/values/styles.xml
+++ b/design/res/values/styles.xml
@@ -117,7 +117,7 @@
<style name="Widget.Design.AppBarLayout" parent="Base.Widget.Design.AppBarLayout">
</style>
- <style name="Widget.Design.CoordinatorLayout" parent="android:Widget">
+ <style name="Widget.Design.CoordinatorLayout" parent="@style/Widget.Support.CoordinatorLayout">
<item name="statusBarBackground">?attr/colorPrimaryDark</item>
</style>
diff --git a/design/res/values/themes.xml b/design/res/values/themes.xml
index a7bd92d..aa4c876 100644
--- a/design/res/values/themes.xml
+++ b/design/res/values/themes.xml
@@ -30,10 +30,12 @@
<style name="Theme.Design" parent="Theme.AppCompat">
<item name="textColorError">?attr/colorError</item>
+ <item name="coordinatorLayoutStyle">@style/Widget.Design.CoordinatorLayout</item>
</style>
<style name="Theme.Design.Light" parent="Theme.AppCompat.Light">
<item name="textColorError">?attr/colorError</item>
+ <item name="coordinatorLayoutStyle">@style/Widget.Design.CoordinatorLayout</item>
</style>
<style name="Theme.Design.NoActionBar">
diff --git a/design/src/android/support/design/widget/CollapsingToolbarLayout.java b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
index 0051de9..8c9b7d4 100644
--- a/design/src/android/support/design/widget/CollapsingToolbarLayout.java
+++ b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
@@ -44,6 +44,7 @@
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.ViewGroupUtils;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.AttributeSet;
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/design/src/android/support/design/widget/CoordinatorLayout.java
deleted file mode 100644
index d97d4e6..0000000
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ /dev/null
@@ -1,3246 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.FloatRange;
-import android.support.annotation.IdRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.math.MathUtils;
-import android.support.v4.util.ObjectsCompat;
-import android.support.v4.util.Pools;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.NestedScrollingParent;
-import android.support.v4.view.NestedScrollingParent2;
-import android.support.v4.view.NestedScrollingParentHelper;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.ViewCompat.NestedScrollType;
-import android.support.v4.view.ViewCompat.ScrollAxis;
-import android.support.v4.view.WindowInsetsCompat;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.ViewTreeObserver;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.reflect.Constructor;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * CoordinatorLayout is a super-powered {@link android.widget.FrameLayout FrameLayout}.
- *
- * <p>CoordinatorLayout is intended for two primary use cases:</p>
- * <ol>
- * <li>As a top-level application decor or chrome layout</li>
- * <li>As a container for a specific interaction with one or more child views</li>
- * </ol>
- *
- * <p>By specifying {@link CoordinatorLayout.Behavior Behaviors} for child views of a
- * CoordinatorLayout you can provide many different interactions within a single parent and those
- * views can also interact with one another. View classes can specify a default behavior when
- * used as a child of a CoordinatorLayout using the
- * {@link CoordinatorLayout.DefaultBehavior DefaultBehavior} annotation.</p>
- *
- * <p>Behaviors may be used to implement a variety of interactions and additional layout
- * modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons
- * that stick to other elements as they move and animate.</p>
- *
- * <p>Children of a CoordinatorLayout may have an
- * {@link CoordinatorLayout.LayoutParams#setAnchorId(int) anchor}. This view id must correspond
- * to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself
- * or a descendant of the anchored child. This can be used to place floating views relative to
- * other arbitrary content panes.</p>
- *
- * <p>Children can specify {@link CoordinatorLayout.LayoutParams#insetEdge} to describe how the
- * view insets the CoordinatorLayout. Any child views which are set to dodge the same inset edges by
- * {@link CoordinatorLayout.LayoutParams#dodgeInsetEdges} will be moved appropriately so that the
- * views do not overlap.</p>
- */
-public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
- static final String TAG = "CoordinatorLayout";
- static final String WIDGET_PACKAGE_NAME;
-
- static {
- final Package pkg = CoordinatorLayout.class.getPackage();
- WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null;
- }
-
- private static final int TYPE_ON_INTERCEPT = 0;
- private static final int TYPE_ON_TOUCH = 1;
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- TOP_SORTED_CHILDREN_COMPARATOR = new ViewElevationComparator();
- } else {
- TOP_SORTED_CHILDREN_COMPARATOR = null;
- }
- }
-
- static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
- Context.class,
- AttributeSet.class
- };
-
- static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors =
- new ThreadLocal<>();
-
- static final int EVENT_PRE_DRAW = 0;
- static final int EVENT_NESTED_SCROLL = 1;
- static final int EVENT_VIEW_REMOVED = 2;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({EVENT_PRE_DRAW, EVENT_NESTED_SCROLL, EVENT_VIEW_REMOVED})
- public @interface DispatchChangeEvent {}
-
- static final Comparator<View> TOP_SORTED_CHILDREN_COMPARATOR;
- private static final Pools.Pool<Rect> sRectPool = new Pools.SynchronizedPool<>(12);
-
- @NonNull
- private static Rect acquireTempRect() {
- Rect rect = sRectPool.acquire();
- if (rect == null) {
- rect = new Rect();
- }
- return rect;
- }
-
- private static void releaseTempRect(@NonNull Rect rect) {
- rect.setEmpty();
- sRectPool.release(rect);
- }
-
- private final List<View> mDependencySortedChildren = new ArrayList<>();
- private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
-
- private final List<View> mTempList1 = new ArrayList<>();
- private final List<View> mTempDependenciesList = new ArrayList<>();
- private final int[] mTempIntPair = new int[2];
- private Paint mScrimPaint;
-
- private boolean mDisallowInterceptReset;
-
- private boolean mIsAttachedToWindow;
-
- private int[] mKeylines;
-
- private View mBehaviorTouchView;
- private View mNestedScrollingTarget;
-
- private OnPreDrawListener mOnPreDrawListener;
- private boolean mNeedsPreDrawListener;
-
- private WindowInsetsCompat mLastInsets;
- private boolean mDrawStatusBarBackground;
- private Drawable mStatusBarBackground;
-
- OnHierarchyChangeListener mOnHierarchyChangeListener;
- private android.support.v4.view.OnApplyWindowInsetsListener mApplyWindowInsetsListener;
-
- private final NestedScrollingParentHelper mNestedScrollingParentHelper =
- new NestedScrollingParentHelper(this);
-
- public CoordinatorLayout(Context context) {
- this(context, null);
- }
-
- public CoordinatorLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
- defStyleAttr, R.style.Widget_Design_CoordinatorLayout);
- final int keylineArrayRes = a.getResourceId(R.styleable.CoordinatorLayout_keylines, 0);
- if (keylineArrayRes != 0) {
- final Resources res = context.getResources();
- mKeylines = res.getIntArray(keylineArrayRes);
- final float density = res.getDisplayMetrics().density;
- final int count = mKeylines.length;
- for (int i = 0; i < count; i++) {
- mKeylines[i] = (int) (mKeylines[i] * density);
- }
- }
- mStatusBarBackground = a.getDrawable(R.styleable.CoordinatorLayout_statusBarBackground);
- a.recycle();
-
- setupForInsets();
- super.setOnHierarchyChangeListener(new HierarchyChangeListener());
- }
-
- @Override
- public void setOnHierarchyChangeListener(OnHierarchyChangeListener onHierarchyChangeListener) {
- mOnHierarchyChangeListener = onHierarchyChangeListener;
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- resetTouchBehaviors(false);
- if (mNeedsPreDrawListener) {
- if (mOnPreDrawListener == null) {
- mOnPreDrawListener = new OnPreDrawListener();
- }
- final ViewTreeObserver vto = getViewTreeObserver();
- vto.addOnPreDrawListener(mOnPreDrawListener);
- }
- if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
- // We're set to fitSystemWindows but we haven't had any insets yet...
- // We should request a new dispatch of window insets
- ViewCompat.requestApplyInsets(this);
- }
- mIsAttachedToWindow = true;
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- resetTouchBehaviors(false);
- if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
- final ViewTreeObserver vto = getViewTreeObserver();
- vto.removeOnPreDrawListener(mOnPreDrawListener);
- }
- if (mNestedScrollingTarget != null) {
- onStopNestedScroll(mNestedScrollingTarget);
- }
- mIsAttachedToWindow = false;
- }
-
- /**
- * Set a drawable to draw in the insets area for the status bar.
- * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
- *
- * @param bg Background drawable to draw behind the status bar
- */
- public void setStatusBarBackground(@Nullable final Drawable bg) {
- if (mStatusBarBackground != bg) {
- if (mStatusBarBackground != null) {
- mStatusBarBackground.setCallback(null);
- }
- mStatusBarBackground = bg != null ? bg.mutate() : null;
- if (mStatusBarBackground != null) {
- if (mStatusBarBackground.isStateful()) {
- mStatusBarBackground.setState(getDrawableState());
- }
- DrawableCompat.setLayoutDirection(mStatusBarBackground,
- ViewCompat.getLayoutDirection(this));
- mStatusBarBackground.setVisible(getVisibility() == VISIBLE, false);
- mStatusBarBackground.setCallback(this);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- /**
- * Gets the drawable used to draw in the insets area for the status bar.
- *
- * @return The status bar background drawable, or null if none set
- */
- @Nullable
- public Drawable getStatusBarBackground() {
- return mStatusBarBackground;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- Drawable d = mStatusBarBackground;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mStatusBarBackground;
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
-
- final boolean visible = visibility == VISIBLE;
- if (mStatusBarBackground != null && mStatusBarBackground.isVisible() != visible) {
- mStatusBarBackground.setVisible(visible, false);
- }
- }
-
- /**
- * Set a drawable to draw in the insets area for the status bar.
- * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
- *
- * @param resId Resource id of a background drawable to draw behind the status bar
- */
- public void setStatusBarBackgroundResource(@DrawableRes int resId) {
- setStatusBarBackground(resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null);
- }
-
- /**
- * Set a drawable to draw in the insets area for the status bar.
- * Note that this will only be activated if this DrawerLayout fitsSystemWindows.
- *
- * @param color Color to use as a background drawable to draw behind the status bar
- * in 0xAARRGGBB format.
- */
- public void setStatusBarBackgroundColor(@ColorInt int color) {
- setStatusBarBackground(new ColorDrawable(color));
- }
-
- final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
- if (!ObjectsCompat.equals(mLastInsets, insets)) {
- mLastInsets = insets;
- mDrawStatusBarBackground = insets != null && insets.getSystemWindowInsetTop() > 0;
- setWillNotDraw(!mDrawStatusBarBackground && getBackground() == null);
-
- // Now dispatch to the Behaviors
- insets = dispatchApplyWindowInsetsToBehaviors(insets);
- requestLayout();
- }
- return insets;
- }
-
- final WindowInsetsCompat getLastWindowInsets() {
- return mLastInsets;
- }
-
- /**
- * Reset all Behavior-related tracking records either to clean up or in preparation
- * for a new event stream. This should be called when attached or detached from a window,
- * in response to an UP or CANCEL event, when intercept is request-disallowed
- * and similar cases where an event stream in progress will be aborted.
- */
- private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior b = lp.getBehavior();
- if (b != null) {
- final long now = SystemClock.uptimeMillis();
- final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
- MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
- if (notifyOnInterceptTouchEvent) {
- b.onInterceptTouchEvent(this, child, cancelEvent);
- } else {
- b.onTouchEvent(this, child, cancelEvent);
- }
- cancelEvent.recycle();
- }
- }
-
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- lp.resetTouchBehaviorTracking();
- }
- mDisallowInterceptReset = false;
- }
-
- /**
- * Populate a list with the current child views, sorted such that the topmost views
- * in z-order are at the front of the list. Useful for hit testing and event dispatch.
- */
- private void getTopSortedChildren(List<View> out) {
- out.clear();
-
- final boolean useCustomOrder = isChildrenDrawingOrderEnabled();
- final int childCount = getChildCount();
- for (int i = childCount - 1; i >= 0; i--) {
- final int childIndex = useCustomOrder ? getChildDrawingOrder(childCount, i) : i;
- final View child = getChildAt(childIndex);
- out.add(child);
- }
-
- if (TOP_SORTED_CHILDREN_COMPARATOR != null) {
- Collections.sort(out, TOP_SORTED_CHILDREN_COMPARATOR);
- }
- }
-
- private boolean performIntercept(MotionEvent ev, final int type) {
- boolean intercepted = false;
- boolean newBlock = false;
-
- MotionEvent cancelEvent = null;
-
- final int action = ev.getActionMasked();
-
- final List<View> topmostChildList = mTempList1;
- getTopSortedChildren(topmostChildList);
-
- // Let topmost child views inspect first
- final int childCount = topmostChildList.size();
- for (int i = 0; i < childCount; i++) {
- final View child = topmostChildList.get(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior b = lp.getBehavior();
-
- if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
- // Cancel all behaviors beneath the one that intercepted.
- // If the event is "down" then we don't have anything to cancel yet.
- if (b != null) {
- if (cancelEvent == null) {
- final long now = SystemClock.uptimeMillis();
- cancelEvent = MotionEvent.obtain(now, now,
- MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
- }
- switch (type) {
- case TYPE_ON_INTERCEPT:
- b.onInterceptTouchEvent(this, child, cancelEvent);
- break;
- case TYPE_ON_TOUCH:
- b.onTouchEvent(this, child, cancelEvent);
- break;
- }
- }
- continue;
- }
-
- if (!intercepted && b != null) {
- switch (type) {
- case TYPE_ON_INTERCEPT:
- intercepted = b.onInterceptTouchEvent(this, child, ev);
- break;
- case TYPE_ON_TOUCH:
- intercepted = b.onTouchEvent(this, child, ev);
- break;
- }
- if (intercepted) {
- mBehaviorTouchView = child;
- }
- }
-
- // Don't keep going if we're not allowing interaction below this.
- // Setting newBlock will make sure we cancel the rest of the behaviors.
- final boolean wasBlocking = lp.didBlockInteraction();
- final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
- newBlock = isBlocking && !wasBlocking;
- if (isBlocking && !newBlock) {
- // Stop here since we don't have anything more to cancel - we already did
- // when the behavior first started blocking things below this point.
- break;
- }
- }
-
- topmostChildList.clear();
-
- return intercepted;
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- MotionEvent cancelEvent = null;
-
- final int action = ev.getActionMasked();
-
- // Make sure we reset in case we had missed a previous important event.
- if (action == MotionEvent.ACTION_DOWN) {
- resetTouchBehaviors(true);
- }
-
- final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
-
- if (cancelEvent != null) {
- cancelEvent.recycle();
- }
-
- if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- resetTouchBehaviors(true);
- }
-
- return intercepted;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- boolean handled = false;
- boolean cancelSuper = false;
- MotionEvent cancelEvent = null;
-
- final int action = ev.getActionMasked();
-
- if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
- // Safe since performIntercept guarantees that
- // mBehaviorTouchView != null if it returns true
- final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
- final Behavior b = lp.getBehavior();
- if (b != null) {
- handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
- }
- }
-
- // Keep the super implementation correct
- if (mBehaviorTouchView == null) {
- handled |= super.onTouchEvent(ev);
- } else if (cancelSuper) {
- if (cancelEvent == null) {
- final long now = SystemClock.uptimeMillis();
- cancelEvent = MotionEvent.obtain(now, now,
- MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
- }
- super.onTouchEvent(cancelEvent);
- }
-
- if (!handled && action == MotionEvent.ACTION_DOWN) {
-
- }
-
- if (cancelEvent != null) {
- cancelEvent.recycle();
- }
-
- if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- resetTouchBehaviors(false);
- }
-
- return handled;
- }
-
- @Override
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- super.requestDisallowInterceptTouchEvent(disallowIntercept);
- if (disallowIntercept && !mDisallowInterceptReset) {
- resetTouchBehaviors(false);
- mDisallowInterceptReset = true;
- }
- }
-
- private int getKeyline(int index) {
- if (mKeylines == null) {
- Log.e(TAG, "No keylines defined for " + this + " - attempted index lookup " + index);
- return 0;
- }
-
- if (index < 0 || index >= mKeylines.length) {
- Log.e(TAG, "Keyline index " + index + " out of range for " + this);
- return 0;
- }
-
- return mKeylines[index];
- }
-
- static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
- if (TextUtils.isEmpty(name)) {
- return null;
- }
-
- final String fullName;
- if (name.startsWith(".")) {
- // Relative to the app package. Prepend the app package name.
- fullName = context.getPackageName() + name;
- } else if (name.indexOf('.') >= 0) {
- // Fully qualified package name.
- fullName = name;
- } else {
- // Assume stock behavior in this package (if we have one)
- fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
- ? (WIDGET_PACKAGE_NAME + '.' + name)
- : name;
- }
-
- try {
- Map<String, Constructor<Behavior>> constructors = sConstructors.get();
- if (constructors == null) {
- constructors = new HashMap<>();
- sConstructors.set(constructors);
- }
- Constructor<Behavior> c = constructors.get(fullName);
- if (c == null) {
- final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
- context.getClassLoader());
- c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
- c.setAccessible(true);
- constructors.put(fullName, c);
- }
- return c.newInstance(context, attrs);
- } catch (Exception e) {
- throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
- }
- }
-
- LayoutParams getResolvedLayoutParams(View child) {
- final LayoutParams result = (LayoutParams) child.getLayoutParams();
- if (!result.mBehaviorResolved) {
- Class<?> childClass = child.getClass();
- DefaultBehavior defaultBehavior = null;
- while (childClass != null &&
- (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
- childClass = childClass.getSuperclass();
- }
- if (defaultBehavior != null) {
- try {
- result.setBehavior(
- defaultBehavior.value().getDeclaredConstructor().newInstance());
- } catch (Exception e) {
- Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
- " could not be instantiated. Did you forget a default constructor?", e);
- }
- }
- result.mBehaviorResolved = true;
- }
- return result;
- }
-
- private void prepareChildren() {
- mDependencySortedChildren.clear();
- mChildDag.clear();
-
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View view = getChildAt(i);
-
- final LayoutParams lp = getResolvedLayoutParams(view);
- lp.findAnchorView(this, view);
-
- mChildDag.addNode(view);
-
- // Now iterate again over the other children, adding any dependencies to the graph
- for (int j = 0; j < count; j++) {
- if (j == i) {
- continue;
- }
- final View other = getChildAt(j);
- if (lp.dependsOn(this, view, other)) {
- if (!mChildDag.contains(other)) {
- // Make sure that the other node is added
- mChildDag.addNode(other);
- }
- // Now add the dependency to the graph
- mChildDag.addEdge(other, view);
- }
- }
- }
-
- // Finally add the sorted graph list to our list
- mDependencySortedChildren.addAll(mChildDag.getSortedList());
- // We also need to reverse the result since we want the start of the list to contain
- // Views which have no dependencies, then dependent views after that
- Collections.reverse(mDependencySortedChildren);
- }
-
- /**
- * Retrieve the transformed bounding rect of an arbitrary descendant view.
- * This does not need to be a direct child.
- *
- * @param descendant descendant view to reference
- * @param out rect to set to the bounds of the descendant view
- */
- void getDescendantRect(View descendant, Rect out) {
- ViewGroupUtils.getDescendantRect(this, descendant, out);
- }
-
- @Override
- protected int getSuggestedMinimumWidth() {
- return Math.max(super.getSuggestedMinimumWidth(), getPaddingLeft() + getPaddingRight());
- }
-
- @Override
- protected int getSuggestedMinimumHeight() {
- return Math.max(super.getSuggestedMinimumHeight(), getPaddingTop() + getPaddingBottom());
- }
-
- /**
- * Called to measure each individual child view unless a
- * {@link CoordinatorLayout.Behavior Behavior} is present. The Behavior may choose to delegate
- * child measurement to this method.
- *
- * @param child the child to measure
- * @param parentWidthMeasureSpec the width requirements for this view
- * @param widthUsed extra space that has been used up by the parent
- * horizontally (possibly by other children of the parent)
- * @param parentHeightMeasureSpec the height requirements for this view
- * @param heightUsed extra space that has been used up by the parent
- * vertically (possibly by other children of the parent)
- */
- public void onMeasureChild(View child, int parentWidthMeasureSpec, int widthUsed,
- int parentHeightMeasureSpec, int heightUsed) {
- measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
- parentHeightMeasureSpec, heightUsed);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- prepareChildren();
- ensurePreDrawListener();
-
- final int paddingLeft = getPaddingLeft();
- final int paddingTop = getPaddingTop();
- final int paddingRight = getPaddingRight();
- final int paddingBottom = getPaddingBottom();
- final int layoutDirection = ViewCompat.getLayoutDirection(this);
- final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- final int widthPadding = paddingLeft + paddingRight;
- final int heightPadding = paddingTop + paddingBottom;
- int widthUsed = getSuggestedMinimumWidth();
- int heightUsed = getSuggestedMinimumHeight();
- int childState = 0;
-
- final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
-
- final int childCount = mDependencySortedChildren.size();
- for (int i = 0; i < childCount; i++) {
- final View child = mDependencySortedChildren.get(i);
- if (child.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
- int keylineWidthUsed = 0;
- if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
- final int keylinePos = getKeyline(lp.keyline);
- final int keylineGravity = GravityCompat.getAbsoluteGravity(
- resolveKeylineGravity(lp.gravity), layoutDirection)
- & Gravity.HORIZONTAL_GRAVITY_MASK;
- if ((keylineGravity == Gravity.LEFT && !isRtl)
- || (keylineGravity == Gravity.RIGHT && isRtl)) {
- keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
- } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
- || (keylineGravity == Gravity.LEFT && isRtl)) {
- keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
- }
- }
-
- int childWidthMeasureSpec = widthMeasureSpec;
- int childHeightMeasureSpec = heightMeasureSpec;
- if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
- // We're set to handle insets but this child isn't, so we will measure the
- // child as if there are no insets
- final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
- + mLastInsets.getSystemWindowInsetRight();
- final int vertInsets = mLastInsets.getSystemWindowInsetTop()
- + mLastInsets.getSystemWindowInsetBottom();
-
- childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
- widthSize - horizInsets, widthMode);
- childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
- heightSize - vertInsets, heightMode);
- }
-
- final Behavior b = lp.getBehavior();
- if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
- childHeightMeasureSpec, 0)) {
- onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
- childHeightMeasureSpec, 0);
- }
-
- widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
- lp.leftMargin + lp.rightMargin);
-
- heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
- lp.topMargin + lp.bottomMargin);
- childState = View.combineMeasuredStates(childState, child.getMeasuredState());
- }
-
- final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
- childState & View.MEASURED_STATE_MASK);
- final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
- childState << View.MEASURED_HEIGHT_STATE_SHIFT);
- setMeasuredDimension(width, height);
- }
-
- private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
- if (insets.isConsumed()) {
- return insets;
- }
-
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (ViewCompat.getFitsSystemWindows(child)) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior b = lp.getBehavior();
-
- if (b != null) {
- // If the view has a behavior, let it try first
- insets = b.onApplyWindowInsets(this, child, insets);
- if (insets.isConsumed()) {
- // If it consumed the insets, break
- break;
- }
- }
- }
- }
-
- return insets;
- }
-
- /**
- * Called to lay out each individual child view unless a
- * {@link CoordinatorLayout.Behavior Behavior} is present. The Behavior may choose to
- * delegate child measurement to this method.
- *
- * @param child child view to lay out
- * @param layoutDirection the resolved layout direction for the CoordinatorLayout, such as
- * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or
- * {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
- */
- public void onLayoutChild(View child, int layoutDirection) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp.checkAnchorChanged()) {
- throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
- + " measurement begins before layout is complete.");
- }
- if (lp.mAnchorView != null) {
- layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
- } else if (lp.keyline >= 0) {
- layoutChildWithKeyline(child, lp.keyline, layoutDirection);
- } else {
- layoutChild(child, layoutDirection);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- final int layoutDirection = ViewCompat.getLayoutDirection(this);
- final int childCount = mDependencySortedChildren.size();
- for (int i = 0; i < childCount; i++) {
- final View child = mDependencySortedChildren.get(i);
- if (child.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior behavior = lp.getBehavior();
-
- if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
- onLayoutChild(child, layoutDirection);
- }
- }
- }
-
- @Override
- public void onDraw(Canvas c) {
- super.onDraw(c);
- if (mDrawStatusBarBackground && mStatusBarBackground != null) {
- final int inset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- if (inset > 0) {
- mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
- mStatusBarBackground.draw(c);
- }
- }
- }
-
- @Override
- public void setFitsSystemWindows(boolean fitSystemWindows) {
- super.setFitsSystemWindows(fitSystemWindows);
- setupForInsets();
- }
-
- /**
- * Mark the last known child position rect for the given child view.
- * This will be used when checking if a child view's position has changed between frames.
- * The rect used here should be one returned by
- * {@link #getChildRect(android.view.View, boolean, android.graphics.Rect)}, with translation
- * disabled.
- *
- * @param child child view to set for
- * @param r rect to set
- */
- void recordLastChildRect(View child, Rect r) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- lp.setLastChildRect(r);
- }
-
- /**
- * Get the last known child rect recorded by
- * {@link #recordLastChildRect(android.view.View, android.graphics.Rect)}.
- *
- * @param child child view to retrieve from
- * @param out rect to set to the outpur values
- */
- void getLastChildRect(View child, Rect out) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- out.set(lp.getLastChildRect());
- }
-
- /**
- * Get the position rect for the given child. If the child has currently requested layout
- * or has a visibility of GONE.
- *
- * @param child child view to check
- * @param transform true to include transformation in the output rect, false to
- * only account for the base position
- * @param out rect to set to the output values
- */
- void getChildRect(View child, boolean transform, Rect out) {
- if (child.isLayoutRequested() || child.getVisibility() == View.GONE) {
- out.setEmpty();
- return;
- }
- if (transform) {
- getDescendantRect(child, out);
- } else {
- out.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
- }
- }
-
- private void getDesiredAnchoredChildRectWithoutConstraints(View child, int layoutDirection,
- Rect anchorRect, Rect out, LayoutParams lp, int childWidth, int childHeight) {
- final int absGravity = GravityCompat.getAbsoluteGravity(
- resolveAnchoredChildGravity(lp.gravity), layoutDirection);
- final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
- resolveGravity(lp.anchorGravity),
- layoutDirection);
-
- final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
- final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int anchorHgrav = absAnchorGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
- final int anchorVgrav = absAnchorGravity & Gravity.VERTICAL_GRAVITY_MASK;
-
- int left;
- int top;
-
- // Align to the anchor. This puts us in an assumed right/bottom child view gravity.
- // If this is not the case we will subtract out the appropriate portion of
- // the child size below.
- switch (anchorHgrav) {
- default:
- case Gravity.LEFT:
- left = anchorRect.left;
- break;
- case Gravity.RIGHT:
- left = anchorRect.right;
- break;
- case Gravity.CENTER_HORIZONTAL:
- left = anchorRect.left + anchorRect.width() / 2;
- break;
- }
-
- switch (anchorVgrav) {
- default:
- case Gravity.TOP:
- top = anchorRect.top;
- break;
- case Gravity.BOTTOM:
- top = anchorRect.bottom;
- break;
- case Gravity.CENTER_VERTICAL:
- top = anchorRect.top + anchorRect.height() / 2;
- break;
- }
-
- // Offset by the child view's gravity itself. The above assumed right/bottom gravity.
- switch (hgrav) {
- default:
- case Gravity.LEFT:
- left -= childWidth;
- break;
- case Gravity.RIGHT:
- // Do nothing, we're already in position.
- break;
- case Gravity.CENTER_HORIZONTAL:
- left -= childWidth / 2;
- break;
- }
-
- switch (vgrav) {
- default:
- case Gravity.TOP:
- top -= childHeight;
- break;
- case Gravity.BOTTOM:
- // Do nothing, we're already in position.
- break;
- case Gravity.CENTER_VERTICAL:
- top -= childHeight / 2;
- break;
- }
-
- out.set(left, top, left + childWidth, top + childHeight);
- }
-
- private void constrainChildRect(LayoutParams lp, Rect out, int childWidth, int childHeight) {
- final int width = getWidth();
- final int height = getHeight();
-
- // Obey margins and padding
- int left = Math.max(getPaddingLeft() + lp.leftMargin,
- Math.min(out.left,
- width - getPaddingRight() - childWidth - lp.rightMargin));
- int top = Math.max(getPaddingTop() + lp.topMargin,
- Math.min(out.top,
- height - getPaddingBottom() - childHeight - lp.bottomMargin));
-
- out.set(left, top, left + childWidth, top + childHeight);
- }
-
- /**
- * Calculate the desired child rect relative to an anchor rect, respecting both
- * gravity and anchorGravity.
- *
- * @param child child view to calculate a rect for
- * @param layoutDirection the desired layout direction for the CoordinatorLayout
- * @param anchorRect rect in CoordinatorLayout coordinates of the anchor view area
- * @param out rect to set to the output values
- */
- void getDesiredAnchoredChildRect(View child, int layoutDirection, Rect anchorRect, Rect out) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
- getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect, out, lp,
- childWidth, childHeight);
- constrainChildRect(lp, out, childWidth, childHeight);
- }
-
- /**
- * CORE ASSUMPTION: anchor has been laid out by the time this is called for a given child view.
- *
- * @param child child to lay out
- * @param anchor view to anchor child relative to; already laid out.
- * @param layoutDirection ViewCompat constant for layout direction
- */
- private void layoutChildWithAnchor(View child, View anchor, int layoutDirection) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
- final Rect anchorRect = acquireTempRect();
- final Rect childRect = acquireTempRect();
- try {
- getDescendantRect(anchor, anchorRect);
- getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, childRect);
- child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
- } finally {
- releaseTempRect(anchorRect);
- releaseTempRect(childRect);
- }
- }
-
- /**
- * Lay out a child view with respect to a keyline.
- *
- * <p>The keyline represents a horizontal offset from the unpadded starting edge of
- * the CoordinatorLayout. The child's gravity will affect how it is positioned with
- * respect to the keyline.</p>
- *
- * @param child child to lay out
- * @param keyline offset from the starting edge in pixels of the keyline to align with
- * @param layoutDirection ViewCompat constant for layout direction
- */
- private void layoutChildWithKeyline(View child, int keyline, int layoutDirection) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int absGravity = GravityCompat.getAbsoluteGravity(
- resolveKeylineGravity(lp.gravity), layoutDirection);
-
- final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
- final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int width = getWidth();
- final int height = getHeight();
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
-
- if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
- keyline = width - keyline;
- }
-
- int left = getKeyline(keyline) - childWidth;
- int top = 0;
-
- switch (hgrav) {
- default:
- case Gravity.LEFT:
- // Nothing to do.
- break;
- case Gravity.RIGHT:
- left += childWidth;
- break;
- case Gravity.CENTER_HORIZONTAL:
- left += childWidth / 2;
- break;
- }
-
- switch (vgrav) {
- default:
- case Gravity.TOP:
- // Do nothing, we're already in position.
- break;
- case Gravity.BOTTOM:
- top += childHeight;
- break;
- case Gravity.CENTER_VERTICAL:
- top += childHeight / 2;
- break;
- }
-
- // Obey margins and padding
- left = Math.max(getPaddingLeft() + lp.leftMargin,
- Math.min(left,
- width - getPaddingRight() - childWidth - lp.rightMargin));
- top = Math.max(getPaddingTop() + lp.topMargin,
- Math.min(top,
- height - getPaddingBottom() - childHeight - lp.bottomMargin));
-
- child.layout(left, top, left + childWidth, top + childHeight);
- }
-
- /**
- * Lay out a child view with no special handling. This will position the child as
- * if it were within a FrameLayout or similar simple frame.
- *
- * @param child child view to lay out
- * @param layoutDirection ViewCompat constant for the desired layout direction
- */
- private void layoutChild(View child, int layoutDirection) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Rect parent = acquireTempRect();
- parent.set(getPaddingLeft() + lp.leftMargin,
- getPaddingTop() + lp.topMargin,
- getWidth() - getPaddingRight() - lp.rightMargin,
- getHeight() - getPaddingBottom() - lp.bottomMargin);
-
- if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
- && !ViewCompat.getFitsSystemWindows(child)) {
- // If we're set to handle insets but this child isn't, then it has been measured as
- // if there are no insets. We need to lay it out to match.
- parent.left += mLastInsets.getSystemWindowInsetLeft();
- parent.top += mLastInsets.getSystemWindowInsetTop();
- parent.right -= mLastInsets.getSystemWindowInsetRight();
- parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
- }
-
- final Rect out = acquireTempRect();
- GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
- child.getMeasuredHeight(), parent, out, layoutDirection);
- child.layout(out.left, out.top, out.right, out.bottom);
-
- releaseTempRect(parent);
- releaseTempRect(out);
- }
-
- /**
- * Return the given gravity value, but if either or both of the axes doesn't have any gravity
- * specified, the default value (start or top) is specified. This should be used for children
- * that are not anchored to another view or a keyline.
- */
- private static int resolveGravity(int gravity) {
- if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
- gravity |= GravityCompat.START;
- }
- if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.NO_GRAVITY) {
- gravity |= Gravity.TOP;
- }
- return gravity;
- }
-
- /**
- * Return the given gravity value or the default if the passed value is NO_GRAVITY.
- * This should be used for children that are positioned relative to a keyline.
- */
- private static int resolveKeylineGravity(int gravity) {
- return gravity == Gravity.NO_GRAVITY ? GravityCompat.END | Gravity.TOP : gravity;
- }
-
- /**
- * Return the given gravity value or the default if the passed value is NO_GRAVITY.
- * This should be used for children that are anchored to another view.
- */
- private static int resolveAnchoredChildGravity(int gravity) {
- return gravity == Gravity.NO_GRAVITY ? Gravity.CENTER : gravity;
- }
-
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp.mBehavior != null) {
- final float scrimAlpha = lp.mBehavior.getScrimOpacity(this, child);
- if (scrimAlpha > 0f) {
- if (mScrimPaint == null) {
- mScrimPaint = new Paint();
- }
- mScrimPaint.setColor(lp.mBehavior.getScrimColor(this, child));
- mScrimPaint.setAlpha(MathUtils.clamp(Math.round(255 * scrimAlpha), 0, 255));
-
- final int saved = canvas.save();
- if (child.isOpaque()) {
- // If the child is opaque, there is no need to draw behind it so we'll inverse
- // clip the canvas
- canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(),
- child.getBottom(), Region.Op.DIFFERENCE);
- }
- // Now draw the rectangle for the scrim
- canvas.drawRect(getPaddingLeft(), getPaddingTop(),
- getWidth() - getPaddingRight(), getHeight() - getPaddingBottom(),
- mScrimPaint);
- canvas.restoreToCount(saved);
- }
- }
- return super.drawChild(canvas, child, drawingTime);
- }
-
- /**
- * Dispatch any dependent view changes to the relevant {@link Behavior} instances.
- *
- * Usually run as part of the pre-draw step when at least one child view has a reported
- * dependency on another view. This allows CoordinatorLayout to account for layout
- * changes and animations that occur outside of the normal layout pass.
- *
- * It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting
- * is completed within the correct coordinate window.
- *
- * The offsetting behavior implemented here does not store the computed offset in
- * the LayoutParams; instead it expects that the layout process will always reconstruct
- * the proper positioning.
- *
- * @param type the type of event which has caused this call
- */
- final void onChildViewsChanged(@DispatchChangeEvent final int type) {
- final int layoutDirection = ViewCompat.getLayoutDirection(this);
- final int childCount = mDependencySortedChildren.size();
- final Rect inset = acquireTempRect();
- final Rect drawRect = acquireTempRect();
- final Rect lastDrawRect = acquireTempRect();
-
- for (int i = 0; i < childCount; i++) {
- final View child = mDependencySortedChildren.get(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
- // Do not try to update GONE child views in pre draw updates.
- continue;
- }
-
- // Check child views before for anchor
- for (int j = 0; j < i; j++) {
- final View checkChild = mDependencySortedChildren.get(j);
-
- if (lp.mAnchorDirectChild == checkChild) {
- offsetChildToAnchor(child, layoutDirection);
- }
- }
-
- // Get the current draw rect of the view
- getChildRect(child, true, drawRect);
-
- // Accumulate inset sizes
- if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
- final int absInsetEdge = GravityCompat.getAbsoluteGravity(
- lp.insetEdge, layoutDirection);
- switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.TOP:
- inset.top = Math.max(inset.top, drawRect.bottom);
- break;
- case Gravity.BOTTOM:
- inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);
- break;
- }
- switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
- case Gravity.LEFT:
- inset.left = Math.max(inset.left, drawRect.right);
- break;
- case Gravity.RIGHT:
- inset.right = Math.max(inset.right, getWidth() - drawRect.left);
- break;
- }
- }
-
- // Dodge inset edges if necessary
- if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
- offsetChildByInset(child, inset, layoutDirection);
- }
-
- if (type != EVENT_VIEW_REMOVED) {
- // Did it change? if not continue
- getLastChildRect(child, lastDrawRect);
- if (lastDrawRect.equals(drawRect)) {
- continue;
- }
- recordLastChildRect(child, drawRect);
- }
-
- // Update any behavior-dependent views for the change
- for (int j = i + 1; j < childCount; j++) {
- final View checkChild = mDependencySortedChildren.get(j);
- final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
- final Behavior b = checkLp.getBehavior();
-
- if (b != null && b.layoutDependsOn(this, checkChild, child)) {
- if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
- // If this is from a pre-draw and we have already been changed
- // from a nested scroll, skip the dispatch and reset the flag
- checkLp.resetChangedAfterNestedScroll();
- continue;
- }
-
- final boolean handled;
- switch (type) {
- case EVENT_VIEW_REMOVED:
- // EVENT_VIEW_REMOVED means that we need to dispatch
- // onDependentViewRemoved() instead
- b.onDependentViewRemoved(this, checkChild, child);
- handled = true;
- break;
- default:
- // Otherwise we dispatch onDependentViewChanged()
- handled = b.onDependentViewChanged(this, checkChild, child);
- break;
- }
-
- if (type == EVENT_NESTED_SCROLL) {
- // If this is from a nested scroll, set the flag so that we may skip
- // any resulting onPreDraw dispatch (if needed)
- checkLp.setChangedAfterNestedScroll(handled);
- }
- }
- }
- }
-
- releaseTempRect(inset);
- releaseTempRect(drawRect);
- releaseTempRect(lastDrawRect);
- }
-
- private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) {
- if (!ViewCompat.isLaidOut(child)) {
- // The view has not been laid out yet, so we can't obtain its bounds.
- return;
- }
-
- if (child.getWidth() <= 0 || child.getHeight() <= 0) {
- // Bounds are empty so there is nothing to dodge against, skip...
- return;
- }
-
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior behavior = lp.getBehavior();
- final Rect dodgeRect = acquireTempRect();
- final Rect bounds = acquireTempRect();
- bounds.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
-
- if (behavior != null && behavior.getInsetDodgeRect(this, child, dodgeRect)) {
- // Make sure that the rect is within the view's bounds
- if (!bounds.contains(dodgeRect)) {
- throw new IllegalArgumentException("Rect should be within the child's bounds."
- + " Rect:" + dodgeRect.toShortString()
- + " | Bounds:" + bounds.toShortString());
- }
- } else {
- dodgeRect.set(bounds);
- }
-
- // We can release the bounds rect now
- releaseTempRect(bounds);
-
- if (dodgeRect.isEmpty()) {
- // Rect is empty so there is nothing to dodge against, skip...
- releaseTempRect(dodgeRect);
- return;
- }
-
- final int absDodgeInsetEdges = GravityCompat.getAbsoluteGravity(lp.dodgeInsetEdges,
- layoutDirection);
-
- boolean offsetY = false;
- if ((absDodgeInsetEdges & Gravity.TOP) == Gravity.TOP) {
- int distance = dodgeRect.top - lp.topMargin - lp.mInsetOffsetY;
- if (distance < inset.top) {
- setInsetOffsetY(child, inset.top - distance);
- offsetY = true;
- }
- }
- if ((absDodgeInsetEdges & Gravity.BOTTOM) == Gravity.BOTTOM) {
- int distance = getHeight() - dodgeRect.bottom - lp.bottomMargin + lp.mInsetOffsetY;
- if (distance < inset.bottom) {
- setInsetOffsetY(child, distance - inset.bottom);
- offsetY = true;
- }
- }
- if (!offsetY) {
- setInsetOffsetY(child, 0);
- }
-
- boolean offsetX = false;
- if ((absDodgeInsetEdges & Gravity.LEFT) == Gravity.LEFT) {
- int distance = dodgeRect.left - lp.leftMargin - lp.mInsetOffsetX;
- if (distance < inset.left) {
- setInsetOffsetX(child, inset.left - distance);
- offsetX = true;
- }
- }
- if ((absDodgeInsetEdges & Gravity.RIGHT) == Gravity.RIGHT) {
- int distance = getWidth() - dodgeRect.right - lp.rightMargin + lp.mInsetOffsetX;
- if (distance < inset.right) {
- setInsetOffsetX(child, distance - inset.right);
- offsetX = true;
- }
- }
- if (!offsetX) {
- setInsetOffsetX(child, 0);
- }
-
- releaseTempRect(dodgeRect);
- }
-
- private void setInsetOffsetX(View child, int offsetX) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp.mInsetOffsetX != offsetX) {
- final int dx = offsetX - lp.mInsetOffsetX;
- ViewCompat.offsetLeftAndRight(child, dx);
- lp.mInsetOffsetX = offsetX;
- }
- }
-
- private void setInsetOffsetY(View child, int offsetY) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp.mInsetOffsetY != offsetY) {
- final int dy = offsetY - lp.mInsetOffsetY;
- ViewCompat.offsetTopAndBottom(child, dy);
- lp.mInsetOffsetY = offsetY;
- }
- }
-
- /**
- * Allows the caller to manually dispatch
- * {@link Behavior#onDependentViewChanged(CoordinatorLayout, View, View)} to the associated
- * {@link Behavior} instances of views which depend on the provided {@link View}.
- *
- * <p>You should not normally need to call this method as the it will be automatically done
- * when the view has changed.
- *
- * @param view the View to find dependents of to dispatch the call.
- */
- public void dispatchDependentViewsChanged(View view) {
- final List<View> dependents = mChildDag.getIncomingEdges(view);
- if (dependents != null && !dependents.isEmpty()) {
- for (int i = 0; i < dependents.size(); i++) {
- final View child = dependents.get(i);
- CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
- child.getLayoutParams();
- CoordinatorLayout.Behavior b = lp.getBehavior();
- if (b != null) {
- b.onDependentViewChanged(this, child, view);
- }
- }
- }
- }
-
- /**
- * Returns the list of views which the provided view depends on. Do not store this list as its
- * contents may not be valid beyond the caller.
- *
- * @param child the view to find dependencies for.
- *
- * @return the list of views which {@code child} depends on.
- */
- @NonNull
- public List<View> getDependencies(@NonNull View child) {
- final List<View> dependencies = mChildDag.getOutgoingEdges(child);
- mTempDependenciesList.clear();
- if (dependencies != null) {
- mTempDependenciesList.addAll(dependencies);
- }
- return mTempDependenciesList;
- }
-
- /**
- * Returns the list of views which depend on the provided view. Do not store this list as its
- * contents may not be valid beyond the caller.
- *
- * @param child the view to find dependents of.
- *
- * @return the list of views which depend on {@code child}.
- */
- @NonNull
- public List<View> getDependents(@NonNull View child) {
- final List<View> edges = mChildDag.getIncomingEdges(child);
- mTempDependenciesList.clear();
- if (edges != null) {
- mTempDependenciesList.addAll(edges);
- }
- return mTempDependenciesList;
- }
-
- @VisibleForTesting
- final List<View> getDependencySortedChildren() {
- prepareChildren();
- return Collections.unmodifiableList(mDependencySortedChildren);
- }
-
- /**
- * Add or remove the pre-draw listener as necessary.
- */
- void ensurePreDrawListener() {
- boolean hasDependencies = false;
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- if (hasDependencies(child)) {
- hasDependencies = true;
- break;
- }
- }
-
- if (hasDependencies != mNeedsPreDrawListener) {
- if (hasDependencies) {
- addPreDrawListener();
- } else {
- removePreDrawListener();
- }
- }
- }
-
- /**
- * Check if the given child has any layout dependencies on other child views.
- */
- private boolean hasDependencies(View child) {
- return mChildDag.hasOutgoingEdges(child);
- }
-
- /**
- * Add the pre-draw listener if we're attached to a window and mark that we currently
- * need it when attached.
- */
- void addPreDrawListener() {
- if (mIsAttachedToWindow) {
- // Add the listener
- if (mOnPreDrawListener == null) {
- mOnPreDrawListener = new OnPreDrawListener();
- }
- final ViewTreeObserver vto = getViewTreeObserver();
- vto.addOnPreDrawListener(mOnPreDrawListener);
- }
-
- // Record that we need the listener regardless of whether or not we're attached.
- // We'll add the real listener when we become attached.
- mNeedsPreDrawListener = true;
- }
-
- /**
- * Remove the pre-draw listener if we're attached to a window and mark that we currently
- * do not need it when attached.
- */
- void removePreDrawListener() {
- if (mIsAttachedToWindow) {
- if (mOnPreDrawListener != null) {
- final ViewTreeObserver vto = getViewTreeObserver();
- vto.removeOnPreDrawListener(mOnPreDrawListener);
- }
- }
- mNeedsPreDrawListener = false;
- }
-
- /**
- * Adjust the child left, top, right, bottom rect to the correct anchor view position,
- * respecting gravity and anchor gravity.
- *
- * Note that child translation properties are ignored in this process, allowing children
- * to be animated away from their anchor. However, if the anchor view is animated,
- * the child will be offset to match the anchor's translated position.
- */
- void offsetChildToAnchor(View child, int layoutDirection) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- if (lp.mAnchorView != null) {
- final Rect anchorRect = acquireTempRect();
- final Rect childRect = acquireTempRect();
- final Rect desiredChildRect = acquireTempRect();
-
- getDescendantRect(lp.mAnchorView, anchorRect);
- getChildRect(child, false, childRect);
-
- int childWidth = child.getMeasuredWidth();
- int childHeight = child.getMeasuredHeight();
- getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
- desiredChildRect, lp, childWidth, childHeight);
- boolean changed = desiredChildRect.left != childRect.left ||
- desiredChildRect.top != childRect.top;
- constrainChildRect(lp, desiredChildRect, childWidth, childHeight);
-
- final int dx = desiredChildRect.left - childRect.left;
- final int dy = desiredChildRect.top - childRect.top;
-
- if (dx != 0) {
- ViewCompat.offsetLeftAndRight(child, dx);
- }
- if (dy != 0) {
- ViewCompat.offsetTopAndBottom(child, dy);
- }
-
- if (changed) {
- // If we have needed to move, make sure to notify the child's Behavior
- final Behavior b = lp.getBehavior();
- if (b != null) {
- b.onDependentViewChanged(this, child, lp.mAnchorView);
- }
- }
-
- releaseTempRect(anchorRect);
- releaseTempRect(childRect);
- releaseTempRect(desiredChildRect);
- }
- }
-
- /**
- * Check if a given point in the CoordinatorLayout's coordinates are within the view bounds
- * of the given direct child view.
- *
- * @param child child view to test
- * @param x X coordinate to test, in the CoordinatorLayout's coordinate system
- * @param y Y coordinate to test, in the CoordinatorLayout's coordinate system
- * @return true if the point is within the child view's bounds, false otherwise
- */
- public boolean isPointInChildBounds(View child, int x, int y) {
- final Rect r = acquireTempRect();
- getDescendantRect(child, r);
- try {
- return r.contains(x, y);
- } finally {
- releaseTempRect(r);
- }
- }
-
- /**
- * Check whether two views overlap each other. The views need to be descendants of this
- * {@link CoordinatorLayout} in the view hierarchy.
- *
- * @param first first child view to test
- * @param second second child view to test
- * @return true if both views are visible and overlap each other
- */
- public boolean doViewsOverlap(View first, View second) {
- if (first.getVisibility() == VISIBLE && second.getVisibility() == VISIBLE) {
- final Rect firstRect = acquireTempRect();
- getChildRect(first, first.getParent() != this, firstRect);
- final Rect secondRect = acquireTempRect();
- getChildRect(second, second.getParent() != this, secondRect);
- try {
- return !(firstRect.left > secondRect.right || firstRect.top > secondRect.bottom
- || firstRect.right < secondRect.left || firstRect.bottom < secondRect.top);
- } finally {
- releaseTempRect(firstRect);
- releaseTempRect(secondRect);
- }
- }
- return false;
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- if (p instanceof LayoutParams) {
- return new LayoutParams((LayoutParams) p);
- } else if (p instanceof MarginLayoutParams) {
- return new LayoutParams((MarginLayoutParams) p);
- }
- return new LayoutParams(p);
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams && super.checkLayoutParams(p);
- }
-
- @Override
- public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
- return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
- }
-
- @Override
- public boolean onStartNestedScroll(View child, View target, int axes, int type) {
- boolean handled = false;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- if (view.getVisibility() == View.GONE) {
- // If it's GONE, don't dispatch
- continue;
- }
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
- target, axes, type);
- handled |= accepted;
- lp.setNestedScrollAccepted(type, accepted);
- } else {
- lp.setNestedScrollAccepted(type, false);
- }
- }
- return handled;
- }
-
- @Override
- public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
- onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
- }
-
- @Override
- public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
- mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
- mNestedScrollingTarget = target;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(type)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- viewBehavior.onNestedScrollAccepted(this, view, child, target,
- nestedScrollAxes, type);
- }
- }
- }
-
- @Override
- public void onStopNestedScroll(View target) {
- onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
- }
-
- @Override
- public void onStopNestedScroll(View target, int type) {
- mNestedScrollingParentHelper.onStopNestedScroll(target, type);
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(type)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- viewBehavior.onStopNestedScroll(this, view, target, type);
- }
- lp.resetNestedScroll(type);
- lp.resetChangedAfterNestedScroll();
- }
- mNestedScrollingTarget = null;
- }
-
- @Override
- public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed) {
- onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
- ViewCompat.TYPE_TOUCH);
- }
-
- @Override
- public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, int type) {
- final int childCount = getChildCount();
- boolean accepted = false;
-
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- if (view.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(type)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
- dxUnconsumed, dyUnconsumed, type);
- accepted = true;
- }
- }
-
- if (accepted) {
- onChildViewsChanged(EVENT_NESTED_SCROLL);
- }
- }
-
- @Override
- public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
- onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
- }
-
- @Override
- public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
- int xConsumed = 0;
- int yConsumed = 0;
- boolean accepted = false;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- if (view.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(type)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- mTempIntPair[0] = mTempIntPair[1] = 0;
- viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
-
- xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
- : Math.min(xConsumed, mTempIntPair[0]);
- yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
- : Math.min(yConsumed, mTempIntPair[1]);
-
- accepted = true;
- }
- }
-
- consumed[0] = xConsumed;
- consumed[1] = yConsumed;
-
- if (accepted) {
- onChildViewsChanged(EVENT_NESTED_SCROLL);
- }
- }
-
- @Override
- public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
- boolean handled = false;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- if (view.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(ViewCompat.TYPE_TOUCH)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
- consumed);
- }
- }
- if (handled) {
- onChildViewsChanged(EVENT_NESTED_SCROLL);
- }
- return handled;
- }
-
- @Override
- public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
- boolean handled = false;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View view = getChildAt(i);
- if (view.getVisibility() == GONE) {
- // If the child is GONE, skip...
- continue;
- }
-
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (!lp.isNestedScrollAccepted(ViewCompat.TYPE_TOUCH)) {
- continue;
- }
-
- final Behavior viewBehavior = lp.getBehavior();
- if (viewBehavior != null) {
- handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
- }
- }
- return handled;
- }
-
- @Override
- public int getNestedScrollAxes() {
- return mNestedScrollingParentHelper.getNestedScrollAxes();
- }
-
- class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
- @Override
- public boolean onPreDraw() {
- onChildViewsChanged(EVENT_PRE_DRAW);
- return true;
- }
- }
-
- /**
- * Sorts child views with higher Z values to the beginning of a collection.
- */
- static class ViewElevationComparator implements Comparator<View> {
- @Override
- public int compare(View lhs, View rhs) {
- final float lz = ViewCompat.getZ(lhs);
- final float rz = ViewCompat.getZ(rhs);
- if (lz > rz) {
- return -1;
- } else if (lz < rz) {
- return 1;
- }
- return 0;
- }
- }
-
- /**
- * Defines the default {@link Behavior} of a {@link View} class.
- *
- * <p>When writing a custom view, use this annotation to define the default behavior
- * when used as a direct child of an {@link CoordinatorLayout}. The default behavior
- * can be overridden using {@link LayoutParams#setBehavior}.</p>
- *
- * <p>Example: <code>@DefaultBehavior(MyBehavior.class)</code></p>
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface DefaultBehavior {
- Class<? extends Behavior> value();
- }
-
- /**
- * Interaction behavior plugin for child views of {@link CoordinatorLayout}.
- *
- * <p>A Behavior implements one or more interactions that a user can take on a child view.
- * These interactions may include drags, swipes, flings, or any other gestures.</p>
- *
- * @param <V> The View type that this Behavior operates on
- */
- public static abstract class Behavior<V extends View> {
-
- /**
- * Default constructor for instantiating Behaviors.
- */
- public Behavior() {
- }
-
- /**
- * Default constructor for inflating Behaviors from layout. The Behavior will have
- * the opportunity to parse specially defined layout parameters. These parameters will
- * appear on the child view tag.
- *
- * @param context
- * @param attrs
- */
- public Behavior(Context context, AttributeSet attrs) {
- }
-
- /**
- * Called when the Behavior has been attached to a LayoutParams instance.
- *
- * <p>This will be called after the LayoutParams has been instantiated and can be
- * modified.</p>
- *
- * @param params the LayoutParams instance that this Behavior has been attached to
- */
- public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
- }
-
- /**
- * Called when the Behavior has been detached from its holding LayoutParams instance.
- *
- * <p>This will only be called if the Behavior has been explicitly removed from the
- * LayoutParams instance via {@link LayoutParams#setBehavior(Behavior)}. It will not be
- * called if the associated view is removed from the CoordinatorLayout or similar.</p>
- */
- public void onDetachedFromLayoutParams() {
- }
-
- /**
- * Respond to CoordinatorLayout touch events before they are dispatched to child views.
- *
- * <p>Behaviors can use this to monitor inbound touch events until one decides to
- * intercept the rest of the event stream to take an action on its associated child view.
- * This method will return false until it detects the proper intercept conditions, then
- * return true once those conditions have occurred.</p>
- *
- * <p>Once a Behavior intercepts touch events, the rest of the event stream will
- * be sent to the {@link #onTouchEvent} method.</p>
- *
- * <p>This method will be called regardless of the visibility of the associated child
- * of the behavior. If you only wish to handle touch events when the child is visible, you
- * should add a check to {@link View#isShown()} on the given child.</p>
- *
- * <p>The default implementation of this method always returns false.</p>
- *
- * @param parent the parent view currently receiving this touch event
- * @param child the child view associated with this Behavior
- * @param ev the MotionEvent describing the touch event being processed
- * @return true if this Behavior would like to intercept and take over the event stream.
- * The default always returns false.
- */
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- return false;
- }
-
- /**
- * Respond to CoordinatorLayout touch events after this Behavior has started
- * {@link #onInterceptTouchEvent intercepting} them.
- *
- * <p>Behaviors may intercept touch events in order to help the CoordinatorLayout
- * manipulate its child views. For example, a Behavior may allow a user to drag a
- * UI pane open or closed. This method should perform actual mutations of view
- * layout state.</p>
- *
- * <p>This method will be called regardless of the visibility of the associated child
- * of the behavior. If you only wish to handle touch events when the child is visible, you
- * should add a check to {@link View#isShown()} on the given child.</p>
- *
- * @param parent the parent view currently receiving this touch event
- * @param child the child view associated with this Behavior
- * @param ev the MotionEvent describing the touch event being processed
- * @return true if this Behavior handled this touch event and would like to continue
- * receiving events in this stream. The default always returns false.
- */
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- return false;
- }
-
- /**
- * Supply a scrim color that will be painted behind the associated child view.
- *
- * <p>A scrim may be used to indicate that the other elements beneath it are not currently
- * interactive or actionable, drawing user focus and attention to the views above the scrim.
- * </p>
- *
- * <p>The default implementation returns {@link Color#BLACK}.</p>
- *
- * @param parent the parent view of the given child
- * @param child the child view above the scrim
- * @return the desired scrim color in 0xAARRGGBB format. The default return value is
- * {@link Color#BLACK}.
- * @see #getScrimOpacity(CoordinatorLayout, android.view.View)
- */
- @ColorInt
- public int getScrimColor(CoordinatorLayout parent, V child) {
- return Color.BLACK;
- }
-
- /**
- * Determine the current opacity of the scrim behind a given child view
- *
- * <p>A scrim may be used to indicate that the other elements beneath it are not currently
- * interactive or actionable, drawing user focus and attention to the views above the scrim.
- * </p>
- *
- * <p>The default implementation returns 0.0f.</p>
- *
- * @param parent the parent view of the given child
- * @param child the child view above the scrim
- * @return the desired scrim opacity from 0.0f to 1.0f. The default return value is 0.0f.
- */
- @FloatRange(from = 0, to = 1)
- public float getScrimOpacity(CoordinatorLayout parent, V child) {
- return 0.f;
- }
-
- /**
- * Determine whether interaction with views behind the given child in the child order
- * should be blocked.
- *
- * <p>The default implementation returns true if
- * {@link #getScrimOpacity(CoordinatorLayout, android.view.View)} would return > 0.0f.</p>
- *
- * @param parent the parent view of the given child
- * @param child the child view to test
- * @return true if {@link #getScrimOpacity(CoordinatorLayout, android.view.View)} would
- * return > 0.0f.
- */
- public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
- return getScrimOpacity(parent, child) > 0.f;
- }
-
- /**
- * Determine whether the supplied child view has another specific sibling view as a
- * layout dependency.
- *
- * <p>This method will be called at least once in response to a layout request. If it
- * returns true for a given child and dependency view pair, the parent CoordinatorLayout
- * will:</p>
- * <ol>
- * <li>Always lay out this child after the dependent child is laid out, regardless
- * of child order.</li>
- * <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
- * position changes.</li>
- * </ol>
- *
- * @param parent the parent view of the given child
- * @param child the child view to test
- * @param dependency the proposed dependency of child
- * @return true if child's layout depends on the proposed dependency's layout,
- * false otherwise
- *
- * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
- */
- public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
- return false;
- }
-
- /**
- * Respond to a change in a child's dependent view
- *
- * <p>This method is called whenever a dependent view changes in size or position outside
- * of the standard layout flow. A Behavior may use this method to appropriately update
- * the child view in response.</p>
- *
- * <p>A view's dependency is determined by
- * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
- * if {@code child} has set another view as it's anchor.</p>
- *
- * <p>Note that if a Behavior changes the layout of a child via this method, it should
- * also be able to reconstruct the correct position in
- * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
- * <code>onDependentViewChanged</code> will not be called during normal layout since
- * the layout of each child view will always happen in dependency order.</p>
- *
- * <p>If the Behavior changes the child view's size or position, it should return true.
- * The default implementation returns false.</p>
- *
- * @param parent the parent view of the given child
- * @param child the child view to manipulate
- * @param dependency the dependent view that changed
- * @return true if the Behavior changed the child view's size or position, false otherwise
- */
- public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
- return false;
- }
-
- /**
- * Respond to a child's dependent view being removed.
- *
- * <p>This method is called after a dependent view has been removed from the parent.
- * A Behavior may use this method to appropriately update the child view in response.</p>
- *
- * <p>A view's dependency is determined by
- * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
- * if {@code child} has set another view as it's anchor.</p>
- *
- * @param parent the parent view of the given child
- * @param child the child view to manipulate
- * @param dependency the dependent view that has been removed
- */
- public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
- }
-
- /**
- * Called when the parent CoordinatorLayout is about to measure the given child view.
- *
- * <p>This method can be used to perform custom or modified measurement of a child view
- * in place of the default child measurement behavior. The Behavior's implementation
- * can delegate to the standard CoordinatorLayout measurement behavior by calling
- * {@link CoordinatorLayout#onMeasureChild(android.view.View, int, int, int, int)
- * parent.onMeasureChild}.</p>
- *
- * @param parent the parent CoordinatorLayout
- * @param child the child to measure
- * @param parentWidthMeasureSpec the width requirements for this view
- * @param widthUsed extra space that has been used up by the parent
- * horizontally (possibly by other children of the parent)
- * @param parentHeightMeasureSpec the height requirements for this view
- * @param heightUsed extra space that has been used up by the parent
- * vertically (possibly by other children of the parent)
- * @return true if the Behavior measured the child view, false if the CoordinatorLayout
- * should perform its default measurement
- */
- public boolean onMeasureChild(CoordinatorLayout parent, V child,
- int parentWidthMeasureSpec, int widthUsed,
- int parentHeightMeasureSpec, int heightUsed) {
- return false;
- }
-
- /**
- * Called when the parent CoordinatorLayout is about the lay out the given child view.
- *
- * <p>This method can be used to perform custom or modified layout of a child view
- * in place of the default child layout behavior. The Behavior's implementation can
- * delegate to the standard CoordinatorLayout measurement behavior by calling
- * {@link CoordinatorLayout#onLayoutChild(android.view.View, int)
- * parent.onLayoutChild}.</p>
- *
- * <p>If a Behavior implements
- * {@link #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)}
- * to change the position of a view in response to a dependent view changing, it
- * should also implement <code>onLayoutChild</code> in such a way that respects those
- * dependent views. <code>onLayoutChild</code> will always be called for a dependent view
- * <em>after</em> its dependency has been laid out.</p>
- *
- * @param parent the parent CoordinatorLayout
- * @param child child view to lay out
- * @param layoutDirection the resolved layout direction for the CoordinatorLayout, such as
- * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or
- * {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
- * @return true if the Behavior performed layout of the child view, false to request
- * default layout behavior
- */
- public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- return false;
- }
-
- // Utility methods for accessing child-specific, behavior-modifiable properties.
-
- /**
- * Associate a Behavior-specific tag object with the given child view.
- * This object will be stored with the child view's LayoutParams.
- *
- * @param child child view to set tag with
- * @param tag tag object to set
- */
- public static void setTag(View child, Object tag) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- lp.mBehaviorTag = tag;
- }
-
- /**
- * Get the behavior-specific tag object with the given child view.
- * This object is stored with the child view's LayoutParams.
- *
- * @param child child view to get tag with
- * @return the previously stored tag object
- */
- public static Object getTag(View child) {
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- return lp.mBehaviorTag;
- }
-
- /**
- * @deprecated You should now override
- * {@link #onStartNestedScroll(CoordinatorLayout, View, View, View, int, int)}. This
- * method will still continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
- */
- @Deprecated
- public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
- @ScrollAxis int axes) {
- return false;
- }
-
- /**
- * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
- *
- * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
- * to this event and return true to indicate that the CoordinatorLayout should act as
- * a nested scrolling parent for this scroll. Only Behaviors that return true from
- * this method will receive subsequent nested scroll events.</p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param directTargetChild the child view of the CoordinatorLayout that either is or
- * contains the target of the nested scroll operation
- * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
- * @param axes the axes that this nested scroll applies to. See
- * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
- * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
- * @param type the type of input which cause this scroll event
- * @return true if the Behavior wishes to accept this nested scroll
- *
- * @see NestedScrollingParent2#onStartNestedScroll(View, View, int, int)
- */
- public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
- @ScrollAxis int axes, @NestedScrollType int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
- target, axes);
- }
- return false;
- }
-
- /**
- * @deprecated You should now override
- * {@link #onNestedScrollAccepted(CoordinatorLayout, View, View, View, int, int)}. This
- * method will still continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
- */
- @Deprecated
- public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
- @ScrollAxis int axes) {
- // Do nothing
- }
-
- /**
- * Called when a nested scroll has been accepted by the CoordinatorLayout.
- *
- * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param directTargetChild the child view of the CoordinatorLayout that either is or
- * contains the target of the nested scroll operation
- * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
- * @param axes the axes that this nested scroll applies to. See
- * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
- * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
- * @param type the type of input which cause this scroll event
- *
- * @see NestedScrollingParent2#onNestedScrollAccepted(View, View, int, int)
- */
- public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
- @ScrollAxis int axes, @NestedScrollType int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
- target, axes);
- }
- }
-
- /**
- * @deprecated You should now override
- * {@link #onStopNestedScroll(CoordinatorLayout, View, View, int)}. This method will still
- * continue to be called if the type is {@link ViewCompat#TYPE_TOUCH}.
- */
- @Deprecated
- public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target) {
- // Do nothing
- }
-
- /**
- * Called when a nested scroll has ended.
- *
- * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * <p><code>onStopNestedScroll</code> marks the end of a single nested scroll event
- * sequence. This is a good place to clean up any state related to the nested scroll.
- * </p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param target the descendant view of the CoordinatorLayout that initiated
- * the nested scroll
- * @param type the type of input which cause this scroll event
- *
- * @see NestedScrollingParent2#onStopNestedScroll(View, int)
- */
- public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target, @NestedScrollType int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- onStopNestedScroll(coordinatorLayout, child, target);
- }
- }
-
- /**
- * @deprecated You should now override
- * {@link #onNestedScroll(CoordinatorLayout, View, View, int, int, int, int, int)}.
- * This method will still continue to be called if the type is
- * {@link ViewCompat#TYPE_TOUCH}.
- */
- @Deprecated
- public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
- @NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed) {
- // Do nothing
- }
-
- /**
- * Called when a nested scroll in progress has updated and the target has scrolled or
- * attempted to scroll.
- *
- * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * <p><code>onNestedScroll</code> is called each time the nested scroll is updated by the
- * nested scrolling child, with both consumed and unconsumed components of the scroll
- * supplied in pixels. <em>Each Behavior responding to the nested scroll will receive the
- * same values.</em>
- * </p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param target the descendant view of the CoordinatorLayout performing the nested scroll
- * @param dxConsumed horizontal pixels consumed by the target's own scrolling operation
- * @param dyConsumed vertical pixels consumed by the target's own scrolling operation
- * @param dxUnconsumed horizontal pixels not consumed by the target's own scrolling
- * operation, but requested by the user
- * @param dyUnconsumed vertical pixels not consumed by the target's own scrolling operation,
- * but requested by the user
- * @param type the type of input which cause this scroll event
- *
- * @see NestedScrollingParent2#onNestedScroll(View, int, int, int, int, int)
- */
- public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
- @NonNull View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
- dxUnconsumed, dyUnconsumed);
- }
- }
-
- /**
- * @deprecated You should now override
- * {@link #onNestedPreScroll(CoordinatorLayout, View, View, int, int, int[], int)}.
- * This method will still continue to be called if the type is
- * {@link ViewCompat#TYPE_TOUCH}.
- */
- @Deprecated
- public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
- // Do nothing
- }
-
- /**
- * Called when a nested scroll in progress is about to update, before the target has
- * consumed any of the scrolled distance.
- *
- * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * <p><code>onNestedPreScroll</code> is called each time the nested scroll is updated
- * by the nested scrolling child, before the nested scrolling child has consumed the scroll
- * distance itself. <em>Each Behavior responding to the nested scroll will receive the
- * same values.</em> The CoordinatorLayout will report as consumed the maximum number
- * of pixels in either direction that any Behavior responding to the nested scroll reported
- * as consumed.</p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param target the descendant view of the CoordinatorLayout performing the nested scroll
- * @param dx the raw horizontal number of pixels that the user attempted to scroll
- * @param dy the raw vertical number of pixels that the user attempted to scroll
- * @param consumed out parameter. consumed[0] should be set to the distance of dx that
- * was consumed, consumed[1] should be set to the distance of dy that
- * was consumed
- * @param type the type of input which cause this scroll event
- *
- * @see NestedScrollingParent2#onNestedPreScroll(View, int, int, int[], int)
- */
- public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
- @NestedScrollType int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
- }
- }
-
- /**
- * Called when a nested scrolling child is starting a fling or an action that would
- * be a fling.
- *
- * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * <p><code>onNestedFling</code> is called when the current nested scrolling child view
- * detects the proper conditions for a fling. It reports if the child itself consumed
- * the fling. If it did not, the child is expected to show some sort of overscroll
- * indication. This method should return true if it consumes the fling, so that a child
- * that did not itself take an action in response can choose not to show an overfling
- * indication.</p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param target the descendant view of the CoordinatorLayout performing the nested scroll
- * @param velocityX horizontal velocity of the attempted fling
- * @param velocityY vertical velocity of the attempted fling
- * @param consumed true if the nested child view consumed the fling
- * @return true if the Behavior consumed the fling
- *
- * @see NestedScrollingParent#onNestedFling(View, float, float, boolean)
- */
- public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target, float velocityX, float velocityY,
- boolean consumed) {
- return false;
- }
-
- /**
- * Called when a nested scrolling child is about to start a fling.
- *
- * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
- * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
- * that returned true will receive subsequent nested scroll events for that nested scroll.
- * </p>
- *
- * <p><code>onNestedPreFling</code> is called when the current nested scrolling child view
- * detects the proper conditions for a fling, but it has not acted on it yet. A
- * Behavior can return true to indicate that it consumed the fling. If at least one
- * Behavior returns true, the fling should not be acted upon by the child.</p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param target the descendant view of the CoordinatorLayout performing the nested scroll
- * @param velocityX horizontal velocity of the attempted fling
- * @param velocityY vertical velocity of the attempted fling
- * @return true if the Behavior consumed the fling
- *
- * @see NestedScrollingParent#onNestedPreFling(View, float, float)
- */
- public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
- @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
- return false;
- }
-
- /**
- * Called when the window insets have changed.
- *
- * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
- * to handle the window inset change on behalf of it's associated view.
- * </p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param insets the new window insets.
- *
- * @return The insets supplied, minus any insets that were consumed
- */
- @NonNull
- public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout,
- V child, WindowInsetsCompat insets) {
- return insets;
- }
-
- /**
- * Called when a child of the view associated with this behavior wants a particular
- * rectangle to be positioned onto the screen.
- *
- * <p>The contract for this method is the same as
- * {@link ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)}.</p>
- *
- * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is
- * associated with
- * @param rectangle The rectangle which the child wishes to be on the screen
- * in the child's coordinates
- * @param immediate true to forbid animated or delayed scrolling, false otherwise
- * @return true if the Behavior handled the request
- * @see ViewParent#requestChildRectangleOnScreen(View, Rect, boolean)
- */
- public boolean onRequestChildRectangleOnScreen(CoordinatorLayout coordinatorLayout,
- V child, Rect rectangle, boolean immediate) {
- return false;
- }
-
- /**
- * Hook allowing a behavior to re-apply a representation of its internal state that had
- * previously been generated by {@link #onSaveInstanceState}. This function will never
- * be called with a null state.
- *
- * @param parent the parent CoordinatorLayout
- * @param child child view to restore from
- * @param state The frozen state that had previously been returned by
- * {@link #onSaveInstanceState}.
- *
- * @see #onSaveInstanceState()
- */
- public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
- // no-op
- }
-
- /**
- * Hook allowing a behavior to generate a representation of its internal state
- * that can later be used to create a new instance with that same state.
- * This state should only contain information that is not persistent or can
- * not be reconstructed later.
- *
- * <p>Behavior state is only saved when both the parent {@link CoordinatorLayout} and
- * a view using this behavior have valid IDs set.</p>
- *
- * @param parent the parent CoordinatorLayout
- * @param child child view to restore from
- *
- * @return Returns a Parcelable object containing the behavior's current dynamic
- * state.
- *
- * @see #onRestoreInstanceState(android.os.Parcelable)
- * @see View#onSaveInstanceState()
- */
- public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
- return BaseSavedState.EMPTY_STATE;
- }
-
- /**
- * Called when a view is set to dodge view insets.
- *
- * <p>This method allows a behavior to update the rectangle that should be dodged.
- * The rectangle should be in the parent's coordinate system and within the child's
- * bounds. If not, a {@link IllegalArgumentException} is thrown.</p>
- *
- * @param parent the CoordinatorLayout parent of the view this Behavior is
- * associated with
- * @param child the child view of the CoordinatorLayout this Behavior is associated with
- * @param rect the rect to update with the dodge rectangle
- * @return true the rect was updated, false if we should use the child's bounds
- */
- public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
- @NonNull Rect rect) {
- return false;
- }
- }
-
- /**
- * Parameters describing the desired layout for a child of a {@link CoordinatorLayout}.
- */
- public static class LayoutParams extends ViewGroup.MarginLayoutParams {
- /**
- * A {@link Behavior} that the child view should obey.
- */
- Behavior mBehavior;
-
- boolean mBehaviorResolved = false;
-
- /**
- * A {@link Gravity} value describing how this child view should lay out.
- * If either or both of the axes are not specified, they are treated by CoordinatorLayout
- * as {@link Gravity#TOP} or {@link GravityCompat#START}. If an
- * {@link #setAnchorId(int) anchor} is also specified, the gravity describes how this child
- * view should be positioned relative to its anchored position.
- */
- public int gravity = Gravity.NO_GRAVITY;
-
- /**
- * A {@link Gravity} value describing which edge of a child view's
- * {@link #getAnchorId() anchor} view the child should position itself relative to.
- */
- public int anchorGravity = Gravity.NO_GRAVITY;
-
- /**
- * The index of the horizontal keyline specified to the parent CoordinatorLayout that this
- * child should align relative to. If an {@link #setAnchorId(int) anchor} is present the
- * keyline will be ignored.
- */
- public int keyline = -1;
-
- /**
- * A {@link View#getId() view id} of a descendant view of the CoordinatorLayout that
- * this child should position relative to.
- */
- int mAnchorId = View.NO_ID;
-
- /**
- * A {@link Gravity} value describing how this child view insets the CoordinatorLayout.
- * Other child views which are set to dodge the same inset edges will be moved appropriately
- * so that the views do not overlap.
- */
- public int insetEdge = Gravity.NO_GRAVITY;
-
- /**
- * A {@link Gravity} value describing how this child view dodges any inset child views in
- * the CoordinatorLayout. Any views which are inset on the same edge as this view is set to
- * dodge will result in this view being moved so that the views do not overlap.
- */
- public int dodgeInsetEdges = Gravity.NO_GRAVITY;
-
- int mInsetOffsetX;
- int mInsetOffsetY;
-
- View mAnchorView;
- View mAnchorDirectChild;
-
- private boolean mDidBlockInteraction;
- private boolean mDidAcceptNestedScrollTouch;
- private boolean mDidAcceptNestedScrollNonTouch;
- private boolean mDidChangeAfterNestedScroll;
-
- final Rect mLastChildRect = new Rect();
-
- Object mBehaviorTag;
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- LayoutParams(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.CoordinatorLayout_Layout);
-
- this.gravity = a.getInteger(
- R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
- Gravity.NO_GRAVITY);
- mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
- View.NO_ID);
- this.anchorGravity = a.getInteger(
- R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
- Gravity.NO_GRAVITY);
-
- this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
- -1);
-
- insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
- dodgeInsetEdges = a.getInt(
- R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
- mBehaviorResolved = a.hasValue(
- R.styleable.CoordinatorLayout_Layout_layout_behavior);
- if (mBehaviorResolved) {
- mBehavior = parseBehavior(context, attrs, a.getString(
- R.styleable.CoordinatorLayout_Layout_layout_behavior));
- }
- a.recycle();
-
- if (mBehavior != null) {
- // If we have a Behavior, dispatch that it has been attached
- mBehavior.onAttachedToLayoutParams(this);
- }
- }
-
- public LayoutParams(LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams p) {
- super(p);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- /**
- * Get the id of this view's anchor.
- *
- * @return A {@link View#getId() view id} or {@link View#NO_ID} if there is no anchor
- */
- @IdRes
- public int getAnchorId() {
- return mAnchorId;
- }
-
- /**
- * Set the id of this view's anchor.
- *
- * <p>The view with this id must be a descendant of the CoordinatorLayout containing
- * the child view this LayoutParams belongs to. It may not be the child view with
- * this LayoutParams or a descendant of it.</p>
- *
- * @param id The {@link View#getId() view id} of the anchor or
- * {@link View#NO_ID} if there is no anchor
- */
- public void setAnchorId(@IdRes int id) {
- invalidateAnchor();
- mAnchorId = id;
- }
-
- /**
- * Get the behavior governing the layout and interaction of the child view within
- * a parent CoordinatorLayout.
- *
- * @return The current behavior or null if no behavior is specified
- */
- @Nullable
- public Behavior getBehavior() {
- return mBehavior;
- }
-
- /**
- * Set the behavior governing the layout and interaction of the child view within
- * a parent CoordinatorLayout.
- *
- * <p>Setting a new behavior will remove any currently associated
- * {@link Behavior#setTag(android.view.View, Object) Behavior tag}.</p>
- *
- * @param behavior The behavior to set or null for no special behavior
- */
- public void setBehavior(@Nullable Behavior behavior) {
- if (mBehavior != behavior) {
- if (mBehavior != null) {
- // First detach any old behavior
- mBehavior.onDetachedFromLayoutParams();
- }
-
- mBehavior = behavior;
- mBehaviorTag = null;
- mBehaviorResolved = true;
-
- if (behavior != null) {
- // Now dispatch that the Behavior has been attached
- behavior.onAttachedToLayoutParams(this);
- }
- }
- }
-
- /**
- * Set the last known position rect for this child view
- * @param r the rect to set
- */
- void setLastChildRect(Rect r) {
- mLastChildRect.set(r);
- }
-
- /**
- * Get the last known position rect for this child view.
- * Note: do not mutate the result of this call.
- */
- Rect getLastChildRect() {
- return mLastChildRect;
- }
-
- /**
- * Returns true if the anchor id changed to another valid view id since the anchor view
- * was resolved.
- */
- boolean checkAnchorChanged() {
- return mAnchorView == null && mAnchorId != View.NO_ID;
- }
-
- /**
- * Returns true if the associated Behavior previously blocked interaction with other views
- * below the associated child since the touch behavior tracking was last
- * {@link #resetTouchBehaviorTracking() reset}.
- *
- * @see #isBlockingInteractionBelow(CoordinatorLayout, android.view.View)
- */
- boolean didBlockInteraction() {
- if (mBehavior == null) {
- mDidBlockInteraction = false;
- }
- return mDidBlockInteraction;
- }
-
- /**
- * Check if the associated Behavior wants to block interaction below the given child
- * view. The given child view should be the child this LayoutParams is associated with.
- *
- * <p>Once interaction is blocked, it will remain blocked until touch interaction tracking
- * is {@link #resetTouchBehaviorTracking() reset}.</p>
- *
- * @param parent the parent CoordinatorLayout
- * @param child the child view this LayoutParams is associated with
- * @return true to block interaction below the given child
- */
- boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
- if (mDidBlockInteraction) {
- return true;
- }
-
- return mDidBlockInteraction |= mBehavior != null
- ? mBehavior.blocksInteractionBelow(parent, child)
- : false;
- }
-
- /**
- * Reset tracking of Behavior-specific touch interactions. This includes
- * interaction blocking.
- *
- * @see #isBlockingInteractionBelow(CoordinatorLayout, android.view.View)
- * @see #didBlockInteraction()
- */
- void resetTouchBehaviorTracking() {
- mDidBlockInteraction = false;
- }
-
- void resetNestedScroll(int type) {
- setNestedScrollAccepted(type, false);
- }
-
- void setNestedScrollAccepted(int type, boolean accept) {
- switch (type) {
- case ViewCompat.TYPE_TOUCH:
- mDidAcceptNestedScrollTouch = accept;
- break;
- case ViewCompat.TYPE_NON_TOUCH:
- mDidAcceptNestedScrollNonTouch = accept;
- break;
- }
- }
-
- boolean isNestedScrollAccepted(int type) {
- switch (type) {
- case ViewCompat.TYPE_TOUCH:
- return mDidAcceptNestedScrollTouch;
- case ViewCompat.TYPE_NON_TOUCH:
- return mDidAcceptNestedScrollNonTouch;
- }
- return false;
- }
-
- boolean getChangedAfterNestedScroll() {
- return mDidChangeAfterNestedScroll;
- }
-
- void setChangedAfterNestedScroll(boolean changed) {
- mDidChangeAfterNestedScroll = changed;
- }
-
- void resetChangedAfterNestedScroll() {
- mDidChangeAfterNestedScroll = false;
- }
-
- /**
- * Check if an associated child view depends on another child view of the CoordinatorLayout.
- *
- * @param parent the parent CoordinatorLayout
- * @param child the child to check
- * @param dependency the proposed dependency to check
- * @return true if child depends on dependency
- */
- boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
- return dependency == mAnchorDirectChild
- || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
- || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
- }
-
- /**
- * Invalidate the cached anchor view and direct child ancestor of that anchor.
- * The anchor will need to be
- * {@link #findAnchorView(CoordinatorLayout, android.view.View) found} before
- * being used again.
- */
- void invalidateAnchor() {
- mAnchorView = mAnchorDirectChild = null;
- }
-
- /**
- * Locate the appropriate anchor view by the current {@link #setAnchorId(int) anchor id}
- * or return the cached anchor view if already known.
- *
- * @param parent the parent CoordinatorLayout
- * @param forChild the child this LayoutParams is associated with
- * @return the located descendant anchor view, or null if the anchor id is
- * {@link View#NO_ID}.
- */
- View findAnchorView(CoordinatorLayout parent, View forChild) {
- if (mAnchorId == View.NO_ID) {
- mAnchorView = mAnchorDirectChild = null;
- return null;
- }
-
- if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {
- resolveAnchorView(forChild, parent);
- }
- return mAnchorView;
- }
-
- /**
- * Determine the anchor view for the child view this LayoutParams is assigned to.
- * Assumes mAnchorId is valid.
- */
- private void resolveAnchorView(final View forChild, final CoordinatorLayout parent) {
- mAnchorView = parent.findViewById(mAnchorId);
- if (mAnchorView != null) {
- if (mAnchorView == parent) {
- if (parent.isInEditMode()) {
- mAnchorView = mAnchorDirectChild = null;
- return;
- }
- throw new IllegalStateException(
- "View can not be anchored to the the parent CoordinatorLayout");
- }
-
- View directChild = mAnchorView;
- for (ViewParent p = mAnchorView.getParent();
- p != parent && p != null;
- p = p.getParent()) {
- if (p == forChild) {
- if (parent.isInEditMode()) {
- mAnchorView = mAnchorDirectChild = null;
- return;
- }
- throw new IllegalStateException(
- "Anchor must not be a descendant of the anchored view");
- }
- if (p instanceof View) {
- directChild = (View) p;
- }
- }
- mAnchorDirectChild = directChild;
- } else {
- if (parent.isInEditMode()) {
- mAnchorView = mAnchorDirectChild = null;
- return;
- }
- throw new IllegalStateException("Could not find CoordinatorLayout descendant view"
- + " with id " + parent.getResources().getResourceName(mAnchorId)
- + " to anchor view " + forChild);
- }
- }
-
- /**
- * Verify that the previously resolved anchor view is still valid - that it is still
- * a descendant of the expected parent view, it is not the child this LayoutParams
- * is assigned to or a descendant of it, and it has the expected id.
- */
- private boolean verifyAnchorView(View forChild, CoordinatorLayout parent) {
- if (mAnchorView.getId() != mAnchorId) {
- return false;
- }
-
- View directChild = mAnchorView;
- for (ViewParent p = mAnchorView.getParent();
- p != parent;
- p = p.getParent()) {
- if (p == null || p == forChild) {
- mAnchorView = mAnchorDirectChild = null;
- return false;
- }
- if (p instanceof View) {
- directChild = (View) p;
- }
- }
- mAnchorDirectChild = directChild;
- return true;
- }
-
- /**
- * Checks whether the view with this LayoutParams should dodge the specified view.
- */
- private boolean shouldDodge(View other, int layoutDirection) {
- LayoutParams lp = (LayoutParams) other.getLayoutParams();
- final int absInset = GravityCompat.getAbsoluteGravity(lp.insetEdge, layoutDirection);
- return absInset != Gravity.NO_GRAVITY && (absInset &
- GravityCompat.getAbsoluteGravity(dodgeInsetEdges, layoutDirection)) == absInset;
- }
- }
-
- private class HierarchyChangeListener implements OnHierarchyChangeListener {
- HierarchyChangeListener() {
- }
-
- @Override
- public void onChildViewAdded(View parent, View child) {
- if (mOnHierarchyChangeListener != null) {
- mOnHierarchyChangeListener.onChildViewAdded(parent, child);
- }
- }
-
- @Override
- public void onChildViewRemoved(View parent, View child) {
- onChildViewsChanged(EVENT_VIEW_REMOVED);
-
- if (mOnHierarchyChangeListener != null) {
- mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
- }
- }
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
-
- final SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
-
- final SparseArray<Parcelable> behaviorStates = ss.behaviorStates;
-
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View child = getChildAt(i);
- final int childId = child.getId();
- final LayoutParams lp = getResolvedLayoutParams(child);
- final Behavior b = lp.getBehavior();
-
- if (childId != NO_ID && b != null) {
- Parcelable savedState = behaviorStates.get(childId);
- if (savedState != null) {
- b.onRestoreInstanceState(this, child, savedState);
- }
- }
- }
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- final SavedState ss = new SavedState(super.onSaveInstanceState());
-
- final SparseArray<Parcelable> behaviorStates = new SparseArray<>();
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View child = getChildAt(i);
- final int childId = child.getId();
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior b = lp.getBehavior();
-
- if (childId != NO_ID && b != null) {
- // If the child has an ID and a Behavior, let it save some state...
- Parcelable state = b.onSaveInstanceState(this, child);
- if (state != null) {
- behaviorStates.append(childId, state);
- }
- }
- }
- ss.behaviorStates = behaviorStates;
- return ss;
- }
-
- @Override
- public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
- final CoordinatorLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final Behavior behavior = lp.getBehavior();
-
- if (behavior != null
- && behavior.onRequestChildRectangleOnScreen(this, child, rectangle, immediate)) {
- return true;
- }
-
- return super.requestChildRectangleOnScreen(child, rectangle, immediate);
- }
-
- private void setupForInsets() {
- if (Build.VERSION.SDK_INT < 21) {
- return;
- }
-
- if (ViewCompat.getFitsSystemWindows(this)) {
- if (mApplyWindowInsetsListener == null) {
- mApplyWindowInsetsListener =
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- return setWindowInsets(insets);
- }
- };
- }
- // First apply the insets listener
- ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener);
-
- // Now set the sys ui flags to enable us to lay out in the window insets
- setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
- } else {
- ViewCompat.setOnApplyWindowInsetsListener(this, null);
- }
- }
-
- protected static class SavedState extends AbsSavedState {
- SparseArray<Parcelable> behaviorStates;
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
-
- final int size = source.readInt();
-
- final int[] ids = new int[size];
- source.readIntArray(ids);
-
- final Parcelable[] states = source.readParcelableArray(loader);
-
- behaviorStates = new SparseArray<>(size);
- for (int i = 0; i < size; i++) {
- behaviorStates.append(ids[i], states[i]);
- }
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
-
- final int size = behaviorStates != null ? behaviorStates.size() : 0;
- dest.writeInt(size);
-
- final int[] ids = new int[size];
- final Parcelable[] states = new Parcelable[size];
-
- for (int i = 0; i < size; i++) {
- ids[i] = behaviorStates.keyAt(i);
- states[i] = behaviorStates.valueAt(i);
- }
- dest.writeIntArray(ids);
- dest.writeParcelableArray(states, flags);
-
- }
-
- public static final Parcelable.Creator<SavedState> CREATOR =
- new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/design/src/android/support/design/widget/DirectedAcyclicGraph.java b/design/src/android/support/design/widget/DirectedAcyclicGraph.java
deleted file mode 100644
index 85a32cd..0000000
--- a/design/src/android/support/design/widget/DirectedAcyclicGraph.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.util.Pools;
-import android.support.v4.util.SimpleArrayMap;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-
-/**
- * A class which represents a simple directed acyclic graph.
- */
-final class DirectedAcyclicGraph<T> {
- private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10);
- private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
-
- private final ArrayList<T> mSortResult = new ArrayList<>();
- private final HashSet<T> mSortTmpMarked = new HashSet<>();
-
- /**
- * Add a node to the graph.
- *
- * <p>If the node already exists in the graph then this method is a no-op.</p>
- *
- * @param node the node to add
- */
- void addNode(@NonNull T node) {
- if (!mGraph.containsKey(node)) {
- mGraph.put(node, null);
- }
- }
-
- /**
- * Returns true if the node is already present in the graph, false otherwise.
- */
- boolean contains(@NonNull T node) {
- return mGraph.containsKey(node);
- }
-
- /**
- * Add an edge to the graph.
- *
- * <p>Both the given nodes should already have been added to the graph through
- * {@link #addNode(Object)}.</p>
- *
- * @param node the parent node
- * @param incomingEdge the node which has is an incoming edge to {@code node}
- */
- void addEdge(@NonNull T node, @NonNull T incomingEdge) {
- if (!mGraph.containsKey(node) || !mGraph.containsKey(incomingEdge)) {
- throw new IllegalArgumentException("All nodes must be present in the graph before"
- + " being added as an edge");
- }
-
- ArrayList<T> edges = mGraph.get(node);
- if (edges == null) {
- // If edges is null, we should try and get one from the pool and add it to the graph
- edges = getEmptyList();
- mGraph.put(node, edges);
- }
- // Finally add the edge to the list
- edges.add(incomingEdge);
- }
-
- /**
- * Get any incoming edges from the given node.
- *
- * @return a list containing any incoming edges, or null if there are none.
- */
- @Nullable
- List getIncomingEdges(@NonNull T node) {
- return mGraph.get(node);
- }
-
- /**
- * Get any outgoing edges for the given node (i.e. nodes which have an incoming edge
- * from the given node).
- *
- * @return a list containing any outgoing edges, or null if there are none.
- */
- @Nullable
- List<T> getOutgoingEdges(@NonNull T node) {
- ArrayList<T> result = null;
- for (int i = 0, size = mGraph.size(); i < size; i++) {
- ArrayList<T> edges = mGraph.valueAt(i);
- if (edges != null && edges.contains(node)) {
- if (result == null) {
- result = new ArrayList<>();
- }
- result.add(mGraph.keyAt(i));
- }
- }
- return result;
- }
-
- boolean hasOutgoingEdges(@NonNull T node) {
- for (int i = 0, size = mGraph.size(); i < size; i++) {
- ArrayList<T> edges = mGraph.valueAt(i);
- if (edges != null && edges.contains(node)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Clears the internal graph, and releases resources to pools.
- */
- void clear() {
- for (int i = 0, size = mGraph.size(); i < size; i++) {
- ArrayList<T> edges = mGraph.valueAt(i);
- if (edges != null) {
- poolList(edges);
- }
- }
- mGraph.clear();
- }
-
- /**
- * Returns a topologically sorted list of the nodes in this graph. This uses the DFS algorithm
- * as described by Cormen et al. (2001). If this graph contains cyclic dependencies then this
- * method will throw a {@link RuntimeException}.
- *
- * <p>The resulting list will be ordered such that index 0 will contain the node at the bottom
- * of the graph. The node at the end of the list will have no dependencies on other nodes.</p>
- */
- @NonNull
- ArrayList<T> getSortedList() {
- mSortResult.clear();
- mSortTmpMarked.clear();
-
- // Start a DFS from each node in the graph
- for (int i = 0, size = mGraph.size(); i < size; i++) {
- dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
- }
-
- return mSortResult;
- }
-
- private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
- if (result.contains(node)) {
- // We've already seen and added the node to the result list, skip...
- return;
- }
- if (tmpMarked.contains(node)) {
- throw new RuntimeException("This graph contains cyclic dependencies");
- }
- // Temporarily mark the node
- tmpMarked.add(node);
- // Recursively dfs all of the node's edges
- final ArrayList<T> edges = mGraph.get(node);
- if (edges != null) {
- for (int i = 0, size = edges.size(); i < size; i++) {
- dfs(edges.get(i), result, tmpMarked);
- }
- }
- // Unmark the node from the temporary list
- tmpMarked.remove(node);
- // Finally add it to the result list
- result.add(node);
- }
-
- /**
- * Returns the size of the graph
- */
- int size() {
- return mGraph.size();
- }
-
- @NonNull
- private ArrayList<T> getEmptyList() {
- ArrayList<T> list = mListPool.acquire();
- if (list == null) {
- list = new ArrayList<>();
- }
- return list;
- }
-
- private void poolList(@NonNull ArrayList<T> list) {
- list.clear();
- mListPool.release(list);
- }
-}
\ No newline at end of file
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index b938836..f37b379 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -36,6 +36,7 @@
import android.support.design.R;
import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
import android.support.v7.widget.AppCompatImageHelper;
import android.util.AttributeSet;
import android.util.Log;
@@ -116,6 +117,11 @@
public static final int SIZE_AUTO = -1;
/**
+ * Indicates that FloatingActionButton should not have a custom size.
+ */
+ public static final int NO_CUSTOM_SIZE = 0;
+
+ /**
* The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal.
*/
private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470;
@@ -132,6 +138,7 @@
private int mBorderWidth;
private int mRippleColor;
private int mSize;
+ private int mCustomSize;
int mImagePadding;
private int mMaxImageSize;
@@ -164,6 +171,8 @@
R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO);
+ mCustomSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabCustomSize,
+ 0);
mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
final float pressedTranslationZ = a.getDimension(
@@ -430,12 +439,41 @@
};
}
+ /**
+ * Sets the size of the button to be a custom value in pixels. If set to
+ * {@link #NO_CUSTOM_SIZE}, custom size will not be used and size will be calculated according
+ * to {@link #setSize(int)} method.
+ *
+ * @param size preferred size in pixels, or zero
+ *
+ * @attr ref android.support.design.R.styleable#FloatingActionButton_fabCustomSize
+ */
+ public void setCustomSize(int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("Custom size should be non-negative.");
+ }
+ mCustomSize = size;
+ }
+
+ /**
+ * Returns the custom size for this button.
+ *
+ * @return size in pixels, or {@link #NO_CUSTOM_SIZE}
+ */
+ public int getCustomSize() {
+ return mCustomSize;
+ }
+
int getSizeDimension() {
return getSizeDimension(mSize);
}
private int getSizeDimension(@Size final int size) {
final Resources res = getResources();
+ // If custom size is set, return it
+ if (mCustomSize != NO_CUSTOM_SIZE) {
+ return mCustomSize;
+ }
switch (size) {
case SIZE_AUTO:
// If we're set to auto, grab the size from resources and refresh
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index 2ed79e4..0540678 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -49,6 +49,7 @@
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.Space;
import android.support.v4.widget.TextViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.AppCompatDrawableManager;
import android.support.v7.widget.AppCompatTextView;
diff --git a/design/src/android/support/design/widget/ViewGroupUtils.java b/design/src/android/support/design/widget/ViewGroupUtils.java
deleted file mode 100644
index 0545516..0000000
--- a/design/src/android/support/design/widget/ViewGroupUtils.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.widget;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-class ViewGroupUtils {
- private static final ThreadLocal<Matrix> sMatrix = new ThreadLocal<>();
- private static final ThreadLocal<RectF> sRectF = new ThreadLocal<>();
-
- /**
- * This is a port of the common
- * {@link ViewGroup#offsetDescendantRectToMyCoords(android.view.View, android.graphics.Rect)}
- * from the framework, but adapted to take transformations into account. The result
- * will be the bounding rect of the real transformed rect.
- *
- * @param descendant view defining the original coordinate system of rect
- * @param rect (in/out) the rect to offset from descendant to this view's coordinate system
- */
- static void offsetDescendantRect(ViewGroup parent, View descendant, Rect rect) {
- Matrix m = sMatrix.get();
- if (m == null) {
- m = new Matrix();
- sMatrix.set(m);
- } else {
- m.reset();
- }
-
- offsetDescendantMatrix(parent, descendant, m);
-
- RectF rectF = sRectF.get();
- if (rectF == null) {
- rectF = new RectF();
- sRectF.set(rectF);
- }
- rectF.set(rect);
- m.mapRect(rectF);
- rect.set((int) (rectF.left + 0.5f), (int) (rectF.top + 0.5f),
- (int) (rectF.right + 0.5f), (int) (rectF.bottom + 0.5f));
- }
-
- /**
- * Retrieve the transformed bounding rect of an arbitrary descendant view.
- * This does not need to be a direct child.
- *
- * @param descendant descendant view to reference
- * @param out rect to set to the bounds of the descendant view
- */
- static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
- out.set(0, 0, descendant.getWidth(), descendant.getHeight());
- offsetDescendantRect(parent, descendant, out);
- }
-
- private static void offsetDescendantMatrix(ViewParent target, View view, Matrix m) {
- final ViewParent parent = view.getParent();
- if (parent instanceof View && parent != target) {
- final View vp = (View) parent;
- offsetDescendantMatrix(target, vp, m);
- m.preTranslate(-vp.getScrollX(), -vp.getScrollY());
- }
-
- m.preTranslate(view.getLeft(), view.getTop());
-
- if (!view.getMatrix().isIdentity()) {
- m.preConcat(view.getMatrix());
- }
- }
-}
diff --git a/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png b/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png
new file mode 100644
index 0000000..ddf2620
--- /dev/null
+++ b/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png
Binary files differ
diff --git a/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png b/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png
new file mode 100644
index 0000000..60f59f5
--- /dev/null
+++ b/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png
Binary files differ
diff --git a/design/tests/res/layout/design_appbar_dodge_left.xml b/design/tests/res/layout/design_appbar_dodge_left.xml
new file mode 100644
index 0000000..7f3ecb9
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_dodge_left.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <include layout="@layout/design_content_appbar_toolbar_collapse_pin" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="bottom|left"
+ android:src="@drawable/ic_album_black_24dp"
+ app:layout_insetEdge="left"
+ android:clickable="true" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab2"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="bottom|left"
+ android:src="@drawable/ic_airplay_black_24dp"
+ app:layout_dodgeInsetEdges="left"
+ android:clickable="true" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/layout/design_appbar_dodge_right.xml b/design/tests/res/layout/design_appbar_dodge_right.xml
new file mode 100644
index 0000000..10815c0
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_dodge_right.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <include layout="@layout/design_content_appbar_toolbar_collapse_pin" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="bottom|right"
+ android:src="@drawable/ic_album_black_24dp"
+ app:layout_insetEdge="right"
+ android:clickable="true" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab2"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="bottom|right"
+ android:src="@drawable/ic_airplay_black_24dp"
+ app:layout_dodgeInsetEdges="right"
+ android:clickable="true" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/values/strings.xml b/design/tests/res/values/strings.xml
index f456921..02763ec 100644
--- a/design/tests/res/values/strings.xml
+++ b/design/tests/res/values/strings.xml
@@ -42,6 +42,9 @@
<string name="design_appbar_anchored_fab_margin_left">AppBar + anchored FAB with left margin</string>
<string name="design_appbar_anchored_fab_margin_right">AppBar + anchored FAB with right margin</string>
+ <string name="design_appbar_dodge_left">AppBar + FABs with dodge on left</string>
+ <string name="design_appbar_dodge_right">AppBar + FABs with dodge on right</string>
+
<string name="textinput_hint">Hint to the user</string>
</resources>
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/testutils/ActivityUtils.java b/design/tests/src/android/support/design/testutils/ActivityUtils.java
deleted file mode 100644
index 1ed6a3f..0000000
--- a/design/tests/src/android/support/design/testutils/ActivityUtils.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.design.testutils;
-
-import static org.junit.Assert.assertTrue;
-
-import android.os.Looper;
-import android.support.test.rule.ActivityTestRule;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Utility methods for testing activities.
- */
-public class ActivityUtils {
- private static final Runnable DO_NOTHING = new Runnable() {
- @Override
- public void run() {
- }
- };
-
- public static void waitForExecution(
- final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) {
- // Wait for two cycles. When starting a postponed transition, it will post to
- // the UI thread and then the execution will be added onto the queue after that.
- // The two-cycle wait makes sure fragments have the opportunity to complete both
- // before returning.
- try {
- rule.runOnUiThread(DO_NOTHING);
- rule.runOnUiThread(DO_NOTHING);
- } catch (Throwable throwable) {
- throw new RuntimeException(throwable);
- }
- }
-
- private static void runOnUiThreadRethrow(
- ActivityTestRule<? extends RecreatedAppCompatActivity> rule, Runnable r) {
- if (Looper.getMainLooper() == Looper.myLooper()) {
- r.run();
- } else {
- try {
- rule.runOnUiThread(r);
- } catch (Throwable t) {
- throw new RuntimeException(t);
- }
- }
- }
-
- /**
- * Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed.
- *
- * @return The newly-restarted RecreatedAppCompatActivity
- */
- public static <T extends RecreatedAppCompatActivity> T recreateActivity(
- ActivityTestRule<? extends RecreatedAppCompatActivity> rule, final T activity)
- throws InterruptedException {
- // Now switch the orientation
- RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
- RecreatedAppCompatActivity.sDestroyed = new CountDownLatch(1);
-
- runOnUiThreadRethrow(rule, new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
- assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
- assertTrue(RecreatedAppCompatActivity.sDestroyed.await(1, TimeUnit.SECONDS));
- T newActivity = (T) RecreatedAppCompatActivity.sActivity;
-
- waitForExecution(rule);
-
- RecreatedAppCompatActivity.clearState();
- return newActivity;
- }
-}
diff --git a/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java b/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
index 97949e6..a166f6b 100644
--- a/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
+++ b/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
@@ -107,6 +107,30 @@
};
}
+ public static ViewAction setCustomSize(final int size) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(FloatingActionButton.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Sets FloatingActionButton custom size";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ final FloatingActionButton fab = (FloatingActionButton) view;
+ fab.setCustomSize(size);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
public static ViewAction setCompatElevation(final float size) {
return new ViewAction() {
@Override
diff --git a/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java b/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
deleted file mode 100644
index 52ba059..0000000
--- a/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.design.testutils;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
-
-import java.util.concurrent.CountDownLatch;
-
-/**
- * Activity that keeps track of resume / destroy lifecycle events, as well as of the last
- * instance of itself.
- */
-public class RecreatedAppCompatActivity extends AppCompatActivity {
- // These must be cleared after each test using clearState()
- public static RecreatedAppCompatActivity sActivity;
- public static CountDownLatch sResumed;
- public static CountDownLatch sDestroyed;
-
- public static void clearState() {
- sActivity = null;
- sResumed = null;
- sDestroyed = null;
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- sActivity = this;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (sResumed != null) {
- sResumed.countDown();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (sDestroyed != null) {
- sDestroyed.countDown();
- }
- }
-}
diff --git a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
index 36cdee6..46bb982 100644
--- a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
+++ b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
@@ -268,6 +268,36 @@
}
/**
+ * Returns a matcher that matches FloatingActionButtons with the specified custom size.
+ */
+ public static Matcher withFabCustomSize(final int customSize) {
+ return new BoundedMatcher<View, View>(View.class) {
+ private String mFailedCheckDescription;
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(mFailedCheckDescription);
+ }
+
+ @Override
+ public boolean matchesSafely(final View view) {
+ if (!(view instanceof FloatingActionButton)) {
+ return false;
+ }
+
+ final FloatingActionButton fab = (FloatingActionButton) view;
+ if (Math.abs(fab.getCustomSize() - customSize) > 1.0f) {
+ mFailedCheckDescription =
+ "Custom size " + fab.getCustomSize() + " is different than expected "
+ + customSize;
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+
+ /**
* Returns a matcher that matches FloatingActionButtons with the specified background
* fill color.
*/
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
index b9a6518..e8a29af 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
@@ -24,8 +24,8 @@
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import android.support.design.test.R;
-import android.support.design.testutils.ActivityUtils;
import android.support.test.filters.LargeTest;
+import android.support.testutils.AppCompatActivityUtils;
import org.junit.Before;
import org.junit.Test;
@@ -59,8 +59,8 @@
.check(matches(hasZ()))
.check(matches(isCollapsed()));
- mActivity = ActivityUtils.recreateActivity(mActivityTestRule, mActivity);
- ActivityUtils.waitForExecution(mActivityTestRule);
+ mActivity = AppCompatActivityUtils.recreateActivity(mActivityTestRule, mActivity);
+ AppCompatActivityUtils.waitForExecution(mActivityTestRule);
// And check that the app bar still is restored correctly
onView(withId(R.id.app_bar))
diff --git a/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java b/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java
new file mode 100644
index 0000000..ad337d5
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.design.test.R;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class AppBarWithDodgingTest extends AppBarLayoutBaseTest {
+ @Test
+ public void testLeftDodge() throws Throwable {
+ configureContent(R.layout.design_appbar_dodge_left,
+ R.string.design_appbar_dodge_left);
+
+ final FloatingActionButton fab = mCoordinatorLayout.findViewById(R.id.fab);
+ final FloatingActionButton fab2 = mCoordinatorLayout.findViewById(R.id.fab2);
+
+ final int[] fabOnScreenXY = new int[2];
+ final int[] fab2OnScreenXY = new int[2];
+ fab.getLocationOnScreen(fabOnScreenXY);
+ fab2.getLocationOnScreen(fab2OnScreenXY);
+
+ final Rect fabRect = new Rect();
+ final Rect fab2Rect = new Rect();
+ fab.getContentRect(fabRect);
+ fab2.getContentRect(fab2Rect);
+
+ // Our second FAB is configured to "dodge" the first one - to be displayed to the
+ // right of it
+ int firstRight = fabOnScreenXY[0] + fabRect.right;
+ int secondLeft = fab2OnScreenXY[0] + fab2Rect.left;
+ assertTrue("Second button left edge at " + secondLeft
+ + " should be dodging the first button right edge at " + firstRight,
+ secondLeft >= firstRight);
+ }
+
+ @Test
+ public void testRightDodge() throws Throwable {
+ configureContent(R.layout.design_appbar_dodge_right,
+ R.string.design_appbar_dodge_right);
+
+ final FloatingActionButton fab = mCoordinatorLayout.findViewById(R.id.fab);
+ final FloatingActionButton fab2 = mCoordinatorLayout.findViewById(R.id.fab2);
+
+ final int[] fabOnScreenXY = new int[2];
+ final int[] fab2OnScreenXY = new int[2];
+ fab.getLocationOnScreen(fabOnScreenXY);
+ fab2.getLocationOnScreen(fab2OnScreenXY);
+
+ final Rect fabRect = new Rect();
+ final Rect fab2Rect = new Rect();
+ fab.getContentRect(fabRect);
+ fab2.getContentRect(fab2Rect);
+
+ // Our second FAB is configured to "dodge" the first one - to be displayed to the
+ // left of it
+ int firstLeft = fabOnScreenXY[0] + fabRect.left;
+ int secondRight = fab2OnScreenXY[0] + fab2Rect.right;
+ assertTrue("Second button right edge at " + secondRight
+ + " should be dodging the first button left edge at " + firstLeft,
+ secondRight <= firstLeft);
+ }
+}
diff --git a/design/tests/src/android/support/design/widget/BaseTestActivity.java b/design/tests/src/android/support/design/widget/BaseTestActivity.java
index 4662001..e1e44e2 100755
--- a/design/tests/src/android/support/design/widget/BaseTestActivity.java
+++ b/design/tests/src/android/support/design/widget/BaseTestActivity.java
@@ -18,7 +18,7 @@
import android.os.Bundle;
import android.support.annotation.LayoutRes;
-import android.support.design.testutils.RecreatedAppCompatActivity;
+import android.support.testutils.RecreatedAppCompatActivity;
import android.view.WindowManager;
abstract class BaseTestActivity extends RecreatedAppCompatActivity {
diff --git a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
index 1037235..f7e9286 100644
--- a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
+++ b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
@@ -20,6 +20,7 @@
import static android.support.design.testutils.FloatingActionButtonActions.setBackgroundTintColor;
import static android.support.design.testutils.FloatingActionButtonActions.setBackgroundTintList;
import static android.support.design.testutils.FloatingActionButtonActions.setCompatElevation;
+import static android.support.design.testutils.FloatingActionButtonActions.setCustomSize;
import static android.support.design.testutils.FloatingActionButtonActions.setImageResource;
import static android.support.design.testutils.FloatingActionButtonActions.setLayoutGravity;
import static android.support.design.testutils.FloatingActionButtonActions.setSize;
@@ -31,6 +32,7 @@
import static android.support.design.testutils.TestUtilsMatchers.withFabBackgroundFill;
import static android.support.design.testutils.TestUtilsMatchers.withFabContentAreaOnMargins;
import static android.support.design.testutils.TestUtilsMatchers.withFabContentHeight;
+import static android.support.design.testutils.TestUtilsMatchers.withFabCustomSize;
import static android.support.design.widget.DesignViewActions.setVisibility;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
@@ -271,4 +273,16 @@
.perform(setEnabled(true))
.perform(setCompatElevation(8));
}
+
+ @SmallTest
+ @Test
+ public void testSetCustomSize() {
+ onView(withId(R.id.fab_standard))
+ .perform(setCustomSize(10))
+ .check(matches(withFabCustomSize(10)));
+
+ onView(withId(R.id.fab_standard))
+ .perform(setCustomSize(20))
+ .check(matches(withFabCustomSize(20)));
+ }
}
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 52471a9..5969235 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -65,8 +65,6 @@
import android.os.Build;
import android.os.Parcelable;
import android.support.design.test.R;
-import android.support.design.testutils.ActivityUtils;
-import android.support.design.testutils.RecreatedAppCompatActivity;
import android.support.design.testutils.TestUtils;
import android.support.design.testutils.ViewStructureImpl;
import android.support.test.annotation.UiThreadTest;
@@ -75,6 +73,8 @@
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
+import android.support.testutils.AppCompatActivityUtils;
+import android.support.testutils.RecreatedAppCompatActivity;
import android.support.v4.widget.TextViewCompat;
import android.text.method.PasswordTransformationMethod;
import android.text.method.TransformationMethod;
@@ -524,8 +524,8 @@
onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
RecreatedAppCompatActivity activity = mActivityTestRule.getActivity();
- activity = ActivityUtils.recreateActivity(mActivityTestRule, activity);
- ActivityUtils.waitForExecution(mActivityTestRule);
+ AppCompatActivityUtils.recreateActivity(mActivityTestRule, activity);
+ AppCompatActivityUtils.waitForExecution(mActivityTestRule);
// Check that the password is still toggled to be shown as plain text
onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
diff --git a/fragment/build.gradle b/fragment/build.gradle
index dfb6ca9..73977c2 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -13,8 +13,11 @@
androidTestImplementation libs.test_runner, { exclude module: 'support-annotations' }
androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
- androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
- androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+ androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+ androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+ androidTestImplementation project(':support-testutils'), {
+ exclude group: 'com.android.support', module: 'support-fragment'
+ }
}
android {
diff --git a/fragment/src/main/java/android/support/v4/app/package.html b/fragment/src/main/java/android/support/v4/app/package.html
deleted file mode 100755
index 02d1b79..0000000
--- a/fragment/src/main/java/android/support/v4/app/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.app classes to assist with development of applications for
-android API level 4 or later. The main features here are backwards-compatible
-versions of {@link android.support.v4.app.FragmentManager} and
-{@link android.support.v4.app.LoaderManager}.
-
-</body>
diff --git a/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java b/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
index eeae2b4..dc62c01 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
@@ -21,6 +21,7 @@
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
import android.support.v4.app.test.NonConfigOnStopActivity;
import org.junit.Rule;
@@ -41,7 +42,7 @@
*/
@Test
public void nonConfigStop() throws Throwable {
- FragmentActivity activity = FragmentTestUtil.recreateActivity(mActivityRule,
+ FragmentActivity activity = FragmentActivityUtils.recreateActivity(mActivityRule,
mActivityRule.getActivity());
// A fragment was added in onStop(), but we shouldn't see it here...
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index 1da1af6..604701f 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -16,7 +16,6 @@
package android.support.v4.app;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
@@ -28,15 +27,12 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.v4.app.test.FragmentTestActivity;
-import android.support.v4.app.test.RecreatedActivity;
import android.util.Pair;
import android.view.ViewGroup;
import android.view.animation.Animation;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
public class FragmentTestUtil {
private static final Runnable DO_NOTHING = new Runnable() {
@@ -247,32 +243,4 @@
}
}
}
-
- /**
- * Restarts the RecreatedActivity and waits for the new activity to be resumed.
- *
- * @return The newly-restarted Activity
- */
- public static <T extends RecreatedActivity> T recreateActivity(
- ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
- throws InterruptedException {
- // Now switch the orientation
- RecreatedActivity.sResumed = new CountDownLatch(1);
- RecreatedActivity.sDestroyed = new CountDownLatch(1);
-
- runOnUiThreadRethrow(rule, new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
- assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
- assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
- T newActivity = (T) RecreatedActivity.sActivity;
-
- waitForExecution(rule);
-
- RecreatedActivity.clearState();
- return newActivity;
- }
}
diff --git a/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java b/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
index e124b67..bf8726f 100644
--- a/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
+++ b/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
@@ -19,6 +19,7 @@
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
import android.support.v4.app.test.HangingFragmentActivity;
import org.junit.Assert;
@@ -37,7 +38,7 @@
@Test
public void testNoCrash() throws InterruptedException {
- HangingFragmentActivity newActivity = FragmentTestUtil.recreateActivity(
+ HangingFragmentActivity newActivity = FragmentActivityUtils.recreateActivity(
mActivityRule, mActivityRule.getActivity());
Assert.assertNotNull(newActivity);
}
diff --git a/fragment/tests/java/android/support/v4/app/LoaderTest.java b/fragment/tests/java/android/support/v4/app/LoaderTest.java
index b581fe7..523baf0 100644
--- a/fragment/tests/java/android/support/v4/app/LoaderTest.java
+++ b/fragment/tests/java/android/support/v4/app/LoaderTest.java
@@ -30,6 +30,7 @@
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
import android.support.v4.app.test.LoaderActivity;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
@@ -58,7 +59,7 @@
public void testLeak() throws Throwable {
// Restart the activity because mActivityRule keeps a strong reference to the
// old activity.
- LoaderActivity activity = FragmentTestUtil.recreateActivity(mActivityRule,
+ LoaderActivity activity = FragmentActivityUtils.recreateActivity(mActivityRule,
mActivityRule.getActivity());
LoaderFragment fragment = new LoaderFragment();
@@ -80,7 +81,7 @@
WeakReference<LoaderActivity> weakActivity = new WeakReference(LoaderActivity.sActivity);
- activity = FragmentTestUtil.recreateActivity(mActivityRule, activity);
+ activity = FragmentActivityUtils.recreateActivity(mActivityRule, activity);
// Wait for everything to settle. We have to make sure that the old Activity
// is ready to be collected.
@@ -101,7 +102,7 @@
assertEquals("Loaded!", activity.textView.getText().toString());
- activity = FragmentTestUtil.recreateActivity(mActivityRule, activity);
+ activity = FragmentActivityUtils.recreateActivity(mActivityRule, activity);
FragmentTestUtil.waitForExecution(mActivityRule);
diff --git a/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java b/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
index 9fab4df..80b9aa5 100644
--- a/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.fragment.test.R;
+import android.support.testutils.RecreatedActivity;
public class HangingFragmentActivity extends RecreatedActivity {
diff --git a/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java b/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
index 8a051f4..2990f0a 100644
--- a/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
@@ -20,6 +20,7 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.fragment.test.R;
+import android.support.testutils.RecreatedActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
diff --git a/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java b/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
index fc03b50..9d71388 100644
--- a/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
@@ -16,6 +16,7 @@
package android.support.v4.app.test;
+import android.support.testutils.RecreatedActivity;
import android.support.v4.app.Fragment;
public class NonConfigOnStopActivity extends RecreatedActivity {
diff --git a/fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java b/fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java
deleted file mode 100644
index c298a88..0000000
--- a/fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.app.test;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.FragmentActivity;
-
-import java.util.concurrent.CountDownLatch;
-
-public class RecreatedActivity extends FragmentActivity {
- // These must be cleared after each test using clearState()
- public static RecreatedActivity sActivity;
- public static CountDownLatch sResumed;
- public static CountDownLatch sDestroyed;
-
- public static void clearState() {
- sActivity = null;
- sResumed = null;
- sDestroyed = null;
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- sActivity = this;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (sResumed != null) {
- sResumed.countDown();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (sDestroyed != null) {
- sDestroyed.countDown();
- }
- }
-}
diff --git a/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java b/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
index 2c7ae41..a34fe2b 100644
--- a/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
+++ b/graphics/drawable/static/src/main/java/android/support/graphics/drawable/VectorDrawableCompat.java
@@ -173,6 +173,10 @@
* <dd>Sets the lineJoin for a stroked path: miter,round,bevel. Default is miter.</dd>
* <dt><code>android:strokeMiterLimit</code></dt>
* <dd>Sets the Miter limit for a stroked path. Default is 4.</dd>
+ * <dt><code>android:fillType</code></dt>
+ * <dd>Sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the
+ * same as SVG's "fill-rule" properties. Default is nonZero. For more details, see
+ * <a href="https://www.w3.org/TR/SVG/painting.html#FillRuleProperty">FillRuleProperty</a></dd>
* </dl></dd>
* </dl>
*
diff --git a/jetifier/.gitignore b/jetifier/.gitignore
new file mode 100644
index 0000000..4469528
--- /dev/null
+++ b/jetifier/.gitignore
@@ -0,0 +1 @@
+**/build
diff --git a/jetifier/jetifier/build.gradle b/jetifier/jetifier/build.gradle
new file mode 100644
index 0000000..752e3aa
--- /dev/null
+++ b/jetifier/jetifier/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+group 'android.support.tools.jetifier'
+version '1.0'
+
+buildscript {
+ ext.supportRootFolder = "${project.projectDir}/../../"
+ apply from: "$supportRootFolder/buildSrc/repos.gradle"
+
+ ext.kotlin_version = '1.1.3'
+
+ repos.addMavenRepositories(repositories)
+
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+subprojects {
+ group 'android.support.tools.jetifier'
+ version '1.0'
+
+ ext.supportRootFolder = "${project.projectDir}/../../.."
+
+ apply plugin: 'kotlin'
+ apply plugin: 'java'
+ apply from: "$supportRootFolder/buildSrc/repos.gradle"
+
+ compileKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+ }
+ compileTestKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+ }
+
+ repos.addMavenRepositories(repositories)
+
+ dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ }
+}
diff --git a/jetifier/jetifier/core/build.gradle b/jetifier/jetifier/core/build.gradle
new file mode 100644
index 0000000..1ac3f36
--- /dev/null
+++ b/jetifier/jetifier/core/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+version '1.0'
+
+dependencies {
+ compile group: 'org.ow2.asm', name: 'asm', version: '5.2'
+ compile group: 'org.ow2.asm', name: 'asm-commons', version: '5.2'
+ compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
+ compile group: 'org.jdom', name: 'jdom2', version: '2.0.6'
+ testCompile group: 'junit', name: 'junit', version: '4.12'
+ testCompile group: 'com.google.truth', name: 'truth', version: '0.31'
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
new file mode 100644
index 0000000..9d74267
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core
+
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.archive.ArchiveItemVisitor
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.Transformer
+import android.support.tools.jetifier.core.transform.bytecode.ByteCodeTransformer
+import android.support.tools.jetifier.core.transform.pom.PomDocument
+import android.support.tools.jetifier.core.transform.pom.PomScanner
+import android.support.tools.jetifier.core.transform.resource.XmlResourcesTransformer
+import android.support.tools.jetifier.core.utils.Log
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * The main entry point to the library. Extracts any given archive recursively and runs all
+ * the registered [Transformer]s over the set and creates new archives that will contain the
+ * transformed files.
+ */
+class Processor(private val config : Config) : ArchiveItemVisitor {
+
+ companion object {
+ private const val TAG = "Processor"
+ }
+
+ private val context = TransformationContext(config)
+
+ private val transformers = listOf(
+ // Register your transformers here
+ ByteCodeTransformer(context),
+ XmlResourcesTransformer(context)
+ )
+
+ /**
+ * Transforms the input libraries given in [inputLibraries] using all the registered
+ * [Transformer]s and returns new libraries stored in [outputPath].
+ *
+ * Currently we have the following transformers:
+ * - [ByteCodeTransformer] for java native code
+ * - [XmlResourcesTransformer] for java native code
+ */
+ fun transform(inputLibraries: List<Path>, outputPath: Path) {
+ // 1) Extract and load all libraries
+ val libraries = loadLibraries(inputLibraries)
+
+ // 2) Search for POM files
+ val pomFiles = scanPomFiles(libraries)
+
+ // 3) Transform all the libraries
+ libraries.forEach{ transformLibrary(it) }
+
+ if (context.wasErrorFound()) {
+ throw IllegalArgumentException("There were ${context.mappingNotFoundFailuresCount}" +
+ " errors found during the remapping. Check the logs for more details.")
+ }
+
+ // TODO: Here we might need to modify the POM files if they point at a library that we have
+ // just refactored.
+
+ // 4) Transform the previously discovered POM files
+ transformPomFiles(pomFiles)
+
+ // 5) Repackage the libraries back to archives
+ libraries.forEach{ it.writeSelfToDir(outputPath) }
+
+ return
+ }
+
+ private fun loadLibraries(inputLibraries : List<Path>) : List<Archive> {
+ val libraries = mutableListOf<Archive>()
+ for (libraryPath in inputLibraries) {
+ if (!Files.isReadable(libraryPath)) {
+ Log.e(TAG, "Cannot access the input file: '%s'", libraryPath)
+ continue
+ }
+
+ libraries.add(Archive.Builder.extract(libraryPath))
+ }
+ return libraries.toList()
+ }
+
+ private fun scanPomFiles(libraries: List<Archive>) : List<PomDocument> {
+ val scanner = PomScanner(config)
+
+ libraries.forEach { scanner.scanArchiveForPomFile(it) }
+ if (scanner.wasErrorFound()) {
+ throw IllegalArgumentException("At least one of the libraries depends on an older" +
+ " version of support library. Check the logs for more details.")
+ }
+
+ return scanner.pomFiles
+ }
+
+ private fun transformPomFiles(files: List<PomDocument>) {
+ files.forEach {
+ it.applyRules(config.pomRewriteRules)
+ it.saveBackToFileIfNeeded()
+ }
+ }
+
+ private fun transformLibrary(archive: Archive) {
+ Log.i(TAG, "Started new transformation")
+ Log.i(TAG, "- Input file: %s", archive.relativePath)
+
+ archive.accept(this)
+ }
+
+ override fun visit(archive: Archive) {
+ archive.files.forEach{ it.accept(this) }
+ }
+
+ override fun visit(archiveFile: ArchiveFile) {
+ val transformer = transformers.firstOrNull { it.canTransform(archiveFile) }
+
+ if (transformer == null) {
+ Log.i(TAG, "[Skipped] %s", archiveFile.relativePath)
+ return
+ }
+
+ Log.i(TAG, "[Applied: %s] %s", transformer.javaClass.simpleName, archiveFile.relativePath)
+ transformer.runTransform(archiveFile)
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
new file mode 100644
index 0000000..19ebe62
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.archive
+
+import android.support.tools.jetifier.core.utils.Log
+import java.io.BufferedOutputStream
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+import java.util.zip.ZipOutputStream
+
+/**
+ * Represents an archive (zip, jar, aar ...)
+ */
+class Archive(
+ override val relativePath: Path,
+ val files: List<ArchiveItem>)
+ : ArchiveItem {
+
+ companion object {
+ /** Defines file extensions that are recognized as archives */
+ val ARCHIVE_EXTENSIONS = listOf(".jar", ".zip", ".aar")
+
+ const val TAG = "Archive"
+ }
+
+ override val fileName: String = relativePath.fileName.toString()
+
+ override fun accept(visitor: ArchiveItemVisitor) {
+ visitor.visit(this)
+ }
+
+ @Throws(IOException::class)
+ fun writeSelfToDir(outputDirPath: Path) {
+ val outputPath = Paths.get(outputDirPath.toString(), fileName)
+
+ if (Files.exists(outputPath)) {
+ Log.i(TAG, "Deleting old output file")
+ Files.delete(outputPath)
+ }
+
+ // Create directories if they don't exist yet
+ Files.createDirectories(outputDirPath)
+
+ Log.i(TAG, "Writing archive: %s", outputPath.toUri())
+ Files.createFile(outputPath)
+ val stream = BufferedOutputStream(FileOutputStream(outputPath.toFile()))
+ writeSelfTo(stream)
+ stream.close()
+ }
+
+ @Throws(IOException::class)
+ override fun writeSelfTo(outputStream: OutputStream) {
+ val out = ZipOutputStream(outputStream)
+
+ for (file in files) {
+ Log.d(TAG, "Writing file: %s", file.relativePath)
+
+ val entry = ZipEntry(file.relativePath.toString())
+ out.putNextEntry(entry)
+ file.writeSelfTo(out)
+ out.closeEntry()
+ }
+ out.finish()
+ }
+
+
+ object Builder {
+
+ @Throws(IOException::class)
+ fun extract(archivePath: Path): Archive {
+ Log.i(TAG, "Extracting: %s", archivePath.toUri())
+
+ val inputStream = FileInputStream(archivePath.toFile())
+ inputStream.use {
+ val archive = extractArchive(it, archivePath)
+ return archive
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun extractArchive(inputStream: InputStream, relativePath: Path)
+ : Archive {
+ val zipIn = ZipInputStream(inputStream)
+ val files = mutableListOf<ArchiveItem>()
+
+ var entry: ZipEntry? = zipIn.nextEntry
+ // iterates over entries in the zip file
+ while (entry != null) {
+ if (!entry.isDirectory) {
+ val entryPath = Paths.get(entry.name)
+ if (isArchive(entry)) {
+ Log.i(TAG, "Extracting nested: %s", entryPath)
+ files.add(extractArchive(zipIn, entryPath))
+ } else {
+ files.add(extractFile(zipIn, entryPath))
+ }
+ }
+ zipIn.closeEntry()
+ entry = zipIn.nextEntry
+ }
+ // Cannot close the zip stream at this moment as that would close also any parent zip
+ // streams in case we are processing a nested archive.
+
+ return Archive(relativePath, files.toList())
+ }
+
+ @Throws(IOException::class)
+ private fun extractFile(zipIn: ZipInputStream, relativePath: Path): ArchiveFile {
+ Log.d(TAG, "Extracting archive: %s", relativePath)
+
+ val data = zipIn.readBytes()
+ return ArchiveFile(relativePath, data)
+ }
+
+ private fun isArchive(zipEntry: ZipEntry) : Boolean {
+ return ARCHIVE_EXTENSIONS.any { zipEntry.name.endsWith(it, ignoreCase = true) }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveFile.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveFile.kt
new file mode 100644
index 0000000..3054b71
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveFile.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.archive
+
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.file.Path
+
+/**
+ * Represents a file in the archive that is not an archive.
+ */
+class ArchiveFile(override val relativePath: Path, var data: ByteArray) : ArchiveItem {
+
+ override val fileName: String = relativePath.fileName.toString()
+
+ override fun accept(visitor: ArchiveItemVisitor) {
+ visitor.visit(this)
+ }
+
+ @Throws(IOException::class)
+ override fun writeSelfTo(outputStream: OutputStream) {
+ outputStream.write(data)
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItem.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItem.kt
new file mode 100644
index 0000000..2d35e13
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItem.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.archive
+
+import java.io.OutputStream
+import java.nio.file.Path
+
+/**
+ * Abstraction to represent archive and its files as a one thing.
+ */
+interface ArchiveItem {
+
+ /**
+ * Relative path of the item according to its location in the archive.
+ *
+ * Files in a nested archive have a path relative to that archive not to the parent of
+ * the archive. The root archive has the file system path set as its relative path.
+ */
+ val relativePath : Path
+
+ /**
+ * Name of the file.
+ */
+ val fileName : String
+
+ /**
+ * Accepts visitor.
+ */
+ fun accept(visitor: ArchiveItemVisitor)
+
+ /**
+ * Writes its internal data (or other nested files) into the given output stream.
+ */
+ fun writeSelfTo(outputStream: OutputStream)
+
+
+ fun isPomFile() = fileName.equals("pom.xml", ignoreCase = true)
+
+ fun isClassFile() = fileName.endsWith(".class", ignoreCase = true)
+
+ fun isXmlFile() = fileName.endsWith(".xml", ignoreCase = true)
+
+ fun isProGuardFile () = fileName.equals("proguard.txt", ignoreCase = true)
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItemVisitor.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItemVisitor.kt
new file mode 100644
index 0000000..7c99fd9
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/ArchiveItemVisitor.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.archive
+
+/**
+ * Visitor for [ArchiveItem]
+ */
+interface ArchiveItemVisitor {
+
+ fun visit(archive: Archive)
+
+ fun visit(archiveFile: ArchiveFile)
+
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/Config.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/Config.kt
new file mode 100644
index 0000000..886a202
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/Config.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.config
+
+import android.support.tools.jetifier.core.rules.RewriteRule
+import android.support.tools.jetifier.core.transform.pom.PomRewriteRule
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.gson.annotations.SerializedName
+
+/**
+ * The main and only one configuration that is used by the tool and all its transformers.
+ *
+ * [restrictToPackagePrefixes] Package prefixes that limit the scope of the rewriting
+ * [rewriteRules] Rules to scan support libraries to generate [TypesMap]
+ * [pomRewriteRules] Rules to rewrite POM files
+ * [typesMap] Map of all java types and fields to be used to rewrite libraries.
+ */
+data class Config(
+ val restrictToPackagePrefixes: List<String>,
+ val rewriteRules: List<RewriteRule>,
+ val pomRewriteRules: List<PomRewriteRule>,
+ val typesMap: TypesMap,
+ val proGuardMap: ProGuardTypesMap) {
+
+ companion object {
+ /** Path to the default config file located within the jar file. */
+ const val DEFAULT_CONFIG_RES_PATH = "/default.config"
+ }
+
+ fun setNewMap(mappings: TypesMap) : Config {
+ return Config(
+ restrictToPackagePrefixes, rewriteRules, pomRewriteRules, mappings, proGuardMap)
+ }
+
+ /** Returns JSON data model of this class */
+ fun toJson() : JsonData {
+ return JsonData(
+ restrictToPackagePrefixes,
+ rewriteRules.map { it.toJson() }.toList(),
+ pomRewriteRules.map { it.toJson() }.toList(),
+ typesMap.toJson(),
+ proGuardMap.toJson()
+ )
+ }
+
+
+ /**
+ * JSON data model for [Config].
+ */
+ data class JsonData(
+ @SerializedName("restrictToPackagePrefixes")
+ val restrictToPackages: List<String?>,
+
+ @SerializedName("rules")
+ val rules: List<RewriteRule.JsonData?>,
+
+ @SerializedName("pomRules")
+ val pomRules: List<PomRewriteRule.JsonData?>,
+
+ @SerializedName("map")
+ val mappings: TypesMap.JsonData? = null,
+
+ @SerializedName("proGuardMap")
+ val proGuardMap: ProGuardTypesMap.JsonData? = null) {
+
+ /** Creates instance of [Config] */
+ fun toConfig() : Config {
+ return Config(
+ restrictToPackages.filterNotNull(),
+ rules.filterNotNull().map { it.toRule() },
+ pomRules.filterNotNull().map { it.toRule() },
+ mappings?.toMappings() ?: TypesMap.EMPTY,
+ proGuardMap?.toMappings() ?: ProGuardTypesMap.EMPTY
+ )
+ }
+ }
+
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/ConfigParser.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/ConfigParser.kt
new file mode 100644
index 0000000..ee6e6c3
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/config/ConfigParser.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.config
+
+import android.support.tools.jetifier.core.utils.Log
+import com.google.gson.GsonBuilder
+import java.io.FileWriter
+import java.nio.file.Files
+import java.nio.file.Path
+
+object ConfigParser {
+
+ private const val TAG : String = "Config"
+
+ private val gson = GsonBuilder().setPrettyPrinting().create()
+
+ fun writeToString(config: Config) : String {
+ return gson.toJson(config.toJson())
+ }
+
+ fun writeToFile(config: Config, outputPath: Path) {
+ FileWriter(outputPath.toFile()).use {
+ gson.toJson(config.toJson(), it)
+ }
+ }
+
+ fun parseFromString(inputText: String) : Config? {
+ return gson.fromJson(inputText, Config.JsonData::class.java).toConfig()
+ }
+
+ fun loadFromFile(configPath: Path) : Config? {
+ return loadConfigFileInternal(configPath)
+ }
+
+ fun loadDefaultConfig() : Config? {
+ Log.v(TAG, "Using the default config '%s'", Config.DEFAULT_CONFIG_RES_PATH)
+
+ val inputStream = javaClass.getResourceAsStream(Config.DEFAULT_CONFIG_RES_PATH)
+ return parseFromString(inputStream.reader().readText())
+ }
+
+ private fun loadConfigFileInternal(configPath: Path) : Config? {
+ if (!Files.isReadable(configPath)) {
+ Log.e(TAG, "Cannot access the config file: '%s'", configPath)
+ return null
+ }
+
+ Log.i(TAG, "Parsing config file: '%s'", configPath.toUri())
+ val config = parseFromString(configPath.toFile().readText())
+
+ if (config == null) {
+ Log.e(TAG, "Failed to parseFromString the config file")
+ return null
+ }
+
+ return config
+ }
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/LibraryMapGenerator.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/LibraryMapGenerator.kt
new file mode 100644
index 0000000..de5a17f
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/LibraryMapGenerator.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.map
+
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.archive.ArchiveItemVisitor
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.transform.Transformer
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+
+/**
+ * Scans a library java files using [MapGeneratorRemapper] to create [TypesMap].
+ */
+class LibraryMapGenerator constructor(config: Config) : ArchiveItemVisitor {
+
+ val remapper = MapGeneratorRemapper(config)
+
+ /**
+ * Scans the given [library] to extend the types map meta-data. The final map can be retrieved
+ * using [generateMap].
+ */
+ fun scanLibrary(library: Archive) {
+ library.accept(this)
+ }
+
+ /**
+ * Creates the [TypesMap] based on the meta-data aggregated via previous [scanFile] calls
+ */
+ fun generateMap() : TypesMap {
+ return remapper.createTypesMap()
+ }
+
+ override fun visit(archive: Archive) {
+ archive.files.forEach{ it.accept(this) }
+ }
+
+ override fun visit(archiveFile: ArchiveFile) {
+ if (archiveFile.isClassFile()) {
+ scanFile(archiveFile)
+ }
+ }
+
+ private fun scanFile(file: ArchiveFile) {
+ val reader = ClassReader(file.data)
+ val writer = ClassWriter(0 /* flags */)
+
+ val visitor = remapper.createClassRemapper(writer)
+
+ reader.accept(visitor, 0 /* flags */)
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/MapGeneratorRemapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/MapGeneratorRemapper.kt
new file mode 100644
index 0000000..43436f6
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/MapGeneratorRemapper.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.map
+
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.bytecode.CoreRemapper
+import android.support.tools.jetifier.core.transform.bytecode.asm.CustomClassRemapper
+import android.support.tools.jetifier.core.transform.bytecode.asm.CustomRemapper
+import android.support.tools.jetifier.core.utils.Log
+import org.objectweb.asm.ClassVisitor
+import java.util.regex.Pattern
+
+/**
+ * Hooks to asm remapping to collect data for [TypesMap] by applying all the [RewriteRule]s from the
+ * given [config] on all discovered and eligible types and fields.
+ */
+class MapGeneratorRemapper(private val config: Config) : CoreRemapper {
+
+ companion object {
+ private const val TAG : String = "MapGeneratorRemapper"
+ }
+
+ private val typesRewritesMap = hashMapOf<JavaType, JavaType>()
+ private val fieldsRewritesMap = hashMapOf<JavaField, JavaField>()
+
+ var isMapNotComplete = false
+ private set
+
+ /**
+ * Ignore mPrefix types and anything that contains $ as these are internal fields that won't be
+ * ever referenced.
+ */
+ private val ignoredFields = Pattern.compile("(^m[A-Z]+.*$)|(.*\\$.*)")
+
+ fun createClassRemapper(visitor: ClassVisitor): CustomClassRemapper {
+ return CustomClassRemapper(visitor, CustomRemapper(this))
+ }
+
+ override fun rewriteType(type: JavaType): JavaType {
+ if (!isTypeSupported(type)) {
+ return type
+ }
+
+ if (typesRewritesMap.contains(type)) {
+ return type
+ }
+
+ // Try to find a rule
+ for (rule in config.rewriteRules) {
+ val mappedTypeName = rule.apply(type) ?: continue
+ typesRewritesMap.put(type, mappedTypeName)
+
+ Log.i(TAG, " map: %s -> %s", type, mappedTypeName)
+ return mappedTypeName
+ }
+
+ isMapNotComplete = true
+ Log.e(TAG, "No rule for: " + type)
+ typesRewritesMap.put(type, type) // Identity
+ return type
+ }
+
+ override fun rewriteField(field : JavaField): JavaField {
+ if (!isTypeSupported(field.owner)) {
+ return field
+ }
+
+ if (ignoredFields.matcher(field.name).matches()) {
+ return field
+ }
+
+ if (fieldsRewritesMap.contains(field)) {
+ return field
+ }
+
+ // Try to find a rule
+ for (rule in config.rewriteRules) {
+ val mappedFieldName = rule.apply(field) ?: continue
+ fieldsRewritesMap.put(field, mappedFieldName)
+
+ Log.i(TAG, " map: %s -> %s", field, mappedFieldName)
+ return mappedFieldName
+ }
+
+ isMapNotComplete = true
+ Log.e(TAG, "No rule for: " + field)
+ fieldsRewritesMap.put(field, field) // Identity
+ return field
+ }
+
+ fun createTypesMap() : TypesMap {
+ return TypesMap(typesRewritesMap, fieldsRewritesMap)
+ }
+
+ private fun isTypeSupported(type: JavaType) : Boolean {
+ return config.restrictToPackagePrefixes.any{ type.fullName.startsWith(it) }
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/TypesMap.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/TypesMap.kt
new file mode 100644
index 0000000..ce02026
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/map/TypesMap.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.map
+
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.rules.RewriteRule
+
+/**
+ * Contains all the mappings needed to rewrite java types and fields.
+ *
+ * These mappings are generated by the preprocessor from existing support libraries and by applying
+ * the given [RewriteRule]s.
+ */
+data class TypesMap(
+ val types: Map<JavaType, JavaType>,
+ val fields: Map<JavaField, JavaField>) {
+
+ companion object {
+ val EMPTY = TypesMap(emptyMap(), emptyMap())
+ }
+
+ /** Returns JSON data model of this class */
+ fun toJson() : JsonData {
+ return JsonData(
+ types = types.map { it.key.fullName to it.value.fullName }
+ .toMap(),
+ fields = mapFields())
+ }
+
+ private fun mapFields() : Map<String, Map<String, List<String>>> {
+ val rawMap = mutableMapOf<String, MutableMap<String, MutableList<String>>>()
+
+ fields.forEach{
+ rawMap
+ .getOrPut(it.key.owner.fullName, { mutableMapOf<String, MutableList<String>>()} )
+ .getOrPut(it.value.owner.fullName, { mutableListOf() })
+ .add(it.key.name)
+ }
+ return rawMap
+ }
+
+ /**
+ * JSON data model for [TypesMap].
+ */
+ data class JsonData(
+ val types: Map<String, String>,
+ val fields: Map<String, Map<String, List<String>>>) {
+
+ /** Creates instance of [TypesMap] */
+ fun toMappings() : TypesMap {
+ return TypesMap(
+ types = types
+ .orEmpty()
+ .map { JavaType(it.key) to JavaType(it.value) }
+ .toMap(),
+ fields = fields
+ .orEmpty().entries
+ .flatMap {
+ top ->
+ top.value.flatMap {
+ mid ->
+ mid.value.map {
+ JavaField(top.key, it) to JavaField(mid.key, it)
+ }
+ }
+ }
+ .toMap())
+ }
+ }
+}
+
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaField.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaField.kt
new file mode 100644
index 0000000..c423f0a
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaField.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.rules
+
+/**
+ * Wrapper for Java field declaration.
+ */
+data class JavaField(val owner : JavaType, val name : String) {
+
+ constructor(owner : String, name : String) : this(JavaType(owner), name)
+
+
+ fun renameOwner(newOwner: JavaType) = JavaField(newOwner, name)
+
+ override fun toString() = owner.toString() + "." + name
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt
new file mode 100644
index 0000000..d7a077b
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.rules
+
+/**
+ * Wrapper for Java type declaration.
+ */
+data class JavaType(val fullName: String) {
+
+ init {
+ if (fullName.contains('.')) {
+ throw IllegalArgumentException("The type does not support '.' as package separator!")
+ }
+ }
+
+ companion object {
+ /** Creates the type from notation where packages are separated using '.' */
+ fun fromDotVersion(fullName: String) : JavaType {
+ return JavaType(fullName.replace('.', '/'))
+ }
+ }
+
+ /** Returns the type as a string where packages are separated using '.' */
+ fun toDotNotation() : String {
+ return fullName.replace('/', '.')
+ }
+
+
+ override fun toString() = fullName
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaTypeXmlRef.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaTypeXmlRef.kt
new file mode 100644
index 0000000..9d58046
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaTypeXmlRef.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.rules
+
+/**
+ * Wrapper for Java type reference used in XML.
+ *
+ * In XML we use '.' as a package separator.
+ */
+data class JavaTypeXmlRef(val fullName : String) {
+
+ constructor(type: JavaType)
+ : this(type.fullName.replace('/', '.'))
+
+ fun toJavaType() : JavaType {
+ return JavaType(fullName.replace('.', '/'))
+ }
+
+ override fun toString() = fullName
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/RewriteRule.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/RewriteRule.kt
new file mode 100644
index 0000000..700e757
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/RewriteRule.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.rules
+
+import com.google.gson.annotations.SerializedName
+import java.util.regex.Pattern
+
+/**
+ * Rule that rewrites a Java type or field based on the given arguments.
+ *
+ * Used in the preprocessor when generating [TypesMap].
+ *
+ * @param from Regular expression where packages are separated via '/' and inner class separator
+ * is "$". Used to match the input type.
+ * @param to A string to be used as a replacement if the 'from' pattern is matched. It can also
+ * apply groups matched from the original pattern using {x} annotation, e.g. {0}.
+ * @param fieldSelectors Collection of regular expressions that are used to match fields. If the
+ * type is matched (using 'from') and the field is matched (or the list of fields selectors is
+ * empty) the field's type gets rewritten according to the 'to' parameter.
+ */
+class RewriteRule(
+ private val from: String,
+ private val to: String,
+ private val fieldSelectors: List<String> = emptyList()) {
+
+ // We escape '$' so we don't conflict with regular expression symbols.
+ private val inputPattern = Pattern.compile("^${from.replace("$", "\\$")}$")
+ private val outputPattern = to.replace("$", "\$")
+
+ private val fields = fieldSelectors.map { Pattern.compile("^$it$") }
+
+ /**
+ * Rewrites the given java type. Returns null if this rule is not applicable for the given type.
+ */
+ fun apply(input: JavaType): JavaType? {
+ if (fields.isNotEmpty()) {
+ return null
+ }
+
+ return applyInternal(input)
+ }
+
+ /**
+ * Rewrites the given field type. Returns null if this rule is not applicable for the given
+ * type.
+ */
+ fun apply(inputField: JavaField) : JavaField? {
+ val typeRewriteResult = applyInternal(inputField.owner) ?: return null
+
+ val isFieldInTheFilter = fields.isEmpty()
+ || fields.any { it.matcher(inputField.name).matches() }
+ if (isFieldInTheFilter) {
+ return inputField.renameOwner(typeRewriteResult)
+ }
+
+ return null
+ }
+
+ private fun applyInternal(input: JavaType): JavaType? {
+ val matcher = inputPattern.matcher(input.fullName)
+ if (!matcher.matches()) {
+ return null
+ }
+
+ var result = outputPattern
+ for (i in 0..matcher.groupCount() - 1) {
+ result = result.replace("{$i}", matcher.group(i + 1))
+ }
+
+ return JavaType(result)
+ }
+
+ override fun toString() : String {
+ return "$inputPattern -> $outputPattern " + fields.joinToString { it.toString() }
+ }
+
+ /** Returns JSON data model of this class */
+ fun toJson() : JsonData {
+ return JsonData(from, to, fieldSelectors)
+ }
+
+
+ /**
+ * JSON data model for [RewriteRule].
+ */
+ data class JsonData(
+ @SerializedName("from")
+ val from: String,
+
+ @SerializedName("to")
+ val to: String,
+
+ @SerializedName("fieldSelectors")
+ val fieldSelectors: List<String>? = null) {
+
+ /** Creates instance of [RewriteRule] */
+ fun toRule() : RewriteRule {
+ return RewriteRule(from, to, fieldSelectors.orEmpty())
+ }
+ }
+
+}
+
+
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
new file mode 100644
index 0000000..3f8af95
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform
+
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.proguard.ProGuardType
+import java.util.regex.Pattern
+
+/**
+ * Context to share the transformation state between individual [Transformer]s.
+ */
+class TransformationContext(val config: Config) {
+
+ // Merges all packages prefixes into one regEx pattern
+ private val packagePrefixPattern = Pattern.compile(
+ "^(" + config.restrictToPackagePrefixes.map { "($it)" }.joinToString("|") + ").*$")
+
+ /** Counter for [reportNoMappingFoundFailure] calls. */
+ var mappingNotFoundFailuresCount = 0
+ private set
+
+ /** Counter for [reportNoProGuardMappingFoundFailure] calls. */
+ var proGuardMappingNotFoundFailuresCount = 0
+ private set
+
+ /** Returns whether any errors were found during the transformation process */
+ fun wasErrorFound() = mappingNotFoundFailuresCount > 0
+
+ /**
+ * Returns whether the given type is eligible for rewrite.
+ *
+ * If not, the transformers should ignore it.
+ */
+ fun isEligibleForRewrite(type: JavaType) : Boolean {
+ if (config.restrictToPackagePrefixes.isEmpty()) {
+ return false
+ }
+ return packagePrefixPattern.matcher(type.fullName).matches()
+ }
+
+ /**
+ * Returns whether the given ProGuard type reference is eligible for rewrite.
+ *
+ * Keep in mind that his has limited capabilities - mainly when * is used as a prefix. Rules
+ * like *.v7 are not matched by prefix support.v7. So don't rely on it and use
+ * the [ProGuardTypesMap] as first.
+ */
+ fun isEligibleForRewrite(type: ProGuardType) : Boolean {
+ if (config.restrictToPackagePrefixes.isEmpty()) {
+ return false
+ }
+ return packagePrefixPattern.matcher(type.value).matches()
+ }
+
+ /**
+ * Used to report that there was a reference found that satisfies [isEligibleForRewrite] but no
+ * mapping was found to rewrite it.
+ */
+ fun reportNoMappingFoundFailure() {
+ mappingNotFoundFailuresCount++
+ }
+
+ /**
+ * Used to report that there was a reference found in the ProGuard file that satisfies
+ * [isEligibleForRewrite] but no mapping was found to rewrite it.
+ */
+ fun reportNoProGuardMappingFoundFailure() {
+ proGuardMappingNotFoundFailuresCount++
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/Transformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/Transformer.kt
new file mode 100644
index 0000000..0c6c8aa
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/Transformer.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+
+/**
+ * Interface to be implemented by any class that wants process files.
+ */
+interface Transformer {
+
+ /**
+ * Returns whether this instance can process the given file.
+ */
+ fun canTransform(file: ArchiveFile): Boolean
+
+ /**
+ * Runs transformation of the given file.
+ */
+ fun runTransform(file: ArchiveFile)
+
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/ByteCodeTransformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/ByteCodeTransformer.kt
new file mode 100644
index 0000000..33235e0
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/ByteCodeTransformer.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.Transformer
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+
+/**
+ * The [Transformer] responsible for java byte code refactoring.
+ */
+class ByteCodeTransformer internal constructor(context: TransformationContext) : Transformer {
+
+ private val remapper: CoreRemapperImpl = CoreRemapperImpl(context)
+
+
+ override fun canTransform(file: ArchiveFile) = file.isClassFile()
+
+ override fun runTransform(file: ArchiveFile) {
+ val reader = ClassReader(file.data)
+ val writer = ClassWriter(0 /* flags */)
+
+ val visitor = remapper.createClassRemapper(writer)
+
+ reader.accept(visitor, 0 /* flags */)
+
+ file.data = writer.toByteArray()
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapper.kt
new file mode 100644
index 0000000..50f3b31
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapper.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode
+
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+
+/**
+ * High-level re-mapping interface to provide only the refactorings needed by jetifier.
+ */
+interface CoreRemapper {
+ fun rewriteType(type: JavaType) : JavaType
+
+ fun rewriteField(field: JavaField) : JavaField
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt
new file mode 100644
index 0000000..486cc25
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode
+
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.bytecode.asm.CustomClassRemapper
+import android.support.tools.jetifier.core.transform.bytecode.asm.CustomRemapper
+import android.support.tools.jetifier.core.utils.Log
+import org.objectweb.asm.ClassVisitor
+
+/**
+ * Applies mappings defined in [TypesMap] during the remapping process.
+ */
+class CoreRemapperImpl(private val context: TransformationContext) : CoreRemapper {
+
+ companion object {
+ const val TAG = "CoreRemapperImpl"
+ }
+
+ private val typesMap = context.config.typesMap
+
+ fun createClassRemapper(visitor: ClassVisitor): CustomClassRemapper {
+ return CustomClassRemapper(visitor, CustomRemapper(this))
+ }
+
+ override fun rewriteType(type: JavaType): JavaType {
+ val result = typesMap.types[type]
+
+ if (!context.isEligibleForRewrite(type)) {
+ return type
+ }
+
+ if (result != null) {
+ Log.i(TAG, " map: %s -> %s", type, result)
+ return result
+ }
+
+ context.reportNoMappingFoundFailure()
+ Log.e(TAG, "No mapping for: " + type)
+ return type
+ }
+
+ override fun rewriteField(field : JavaField): JavaField {
+ val result = typesMap.fields[field]
+
+ if (!context.isEligibleForRewrite(field.owner)) {
+ return field
+ }
+
+ if (result != null) {
+ Log.i(TAG, " map: %s -> %s", field, result)
+ return result
+ }
+
+ context.reportNoMappingFoundFailure()
+ Log.e(TAG, "No mapping for: " + field)
+ return field
+ }
+
+}
+
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomClassRemapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomClassRemapper.kt
new file mode 100644
index 0000000..692e65d
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomClassRemapper.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode.asm
+
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.FieldVisitor
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.commons.ClassRemapper
+
+/**
+ * Currently only adds field context awareness into [ClassRemapper] and substitutes the default
+ * method remapper with [CustomMethodRemapper]
+ */
+class CustomClassRemapper(cv: ClassVisitor, private val customRemapper: CustomRemapper)
+ : ClassRemapper(Opcodes.ASM5, cv, customRemapper) {
+
+ override fun visitField(access: Int,
+ name: String,
+ desc: String?,
+ signature: String?,
+ value: Any?) : FieldVisitor? {
+ cv ?: return null
+
+ val field = customRemapper.mapField(className, name)
+ val fieldVisitor = cv.visitField(
+ access,
+ field.name,
+ remapper.mapDesc(desc),
+ remapper.mapSignature(signature, true),
+ remapper.mapValue(value))
+
+ fieldVisitor ?: return null
+
+ return createFieldRemapper(fieldVisitor)
+ }
+
+ override fun createMethodRemapper(mv: MethodVisitor) : MethodVisitor {
+ return CustomMethodRemapper(mv, customRemapper)
+ }
+}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomMethodRemapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomMethodRemapper.kt
new file mode 100644
index 0000000..cc60cbf
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomMethodRemapper.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode.asm
+
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.commons.MethodRemapper
+
+/**
+ * Currently only adds field context awareness into [MethodRemapper]
+ */
+internal class CustomMethodRemapper(mv:MethodVisitor,
+ private val customRemapper: CustomRemapper)
+ : MethodRemapper(Opcodes.ASM5, mv, customRemapper) {
+
+ override fun visitFieldInsn(opcode: Int, owner: String, name: String, desc: String?) {
+ mv ?: return
+
+ val field = customRemapper.mapField(owner, name)
+ mv.visitFieldInsn(opcode, field.owner.fullName, field.name, remapper.mapDesc(desc))
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomRemapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomRemapper.kt
new file mode 100644
index 0000000..5debf70
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/asm/CustomRemapper.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.bytecode.asm
+
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.bytecode.CoreRemapper
+import org.objectweb.asm.commons.Remapper
+
+/**
+ * Extends [Remapper] with a capability to rewrite field names together with their owner.
+ */
+class CustomRemapper(val remapperImpl: CoreRemapper) : Remapper() {
+
+ override fun map(typeName: String): String {
+ return remapperImpl.rewriteType(JavaType(typeName)).fullName
+ }
+
+ fun mapField(ownerName: String, fieldName: String): JavaField {
+ return remapperImpl.rewriteField(JavaField(ownerName, fieldName))
+ }
+
+ override fun mapFieldName(owner: String?, name: String, desc: String?): String {
+ throw RuntimeException("This should not be called")
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDependency.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDependency.kt
new file mode 100644
index 0000000..1622fd7
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDependency.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import com.google.gson.annotations.SerializedName
+import org.jdom2.Document
+import org.jdom2.Element
+
+/**
+ * Represents a '<dependency>' XML node of a POM file.
+ *
+ * See documentation of the content at https://maven.apache.org/pom.html#Dependencies
+ */
+data class PomDependency(
+ @SerializedName("groupId")
+ val groupId: String? = null,
+
+ @SerializedName("artifactId")
+ val artifactId: String? = null,
+
+ @SerializedName("version")
+ var version: String? = null,
+
+ @SerializedName("classifier")
+ val classifier: String? = null,
+
+ @SerializedName("type")
+ val type: String? = null,
+
+ @SerializedName("scope")
+ val scope: String? = null,
+
+ @SerializedName("systemPath")
+ val systemPath: String? = null,
+
+ @SerializedName("optional")
+ val optional: String? = null) {
+
+ companion object {
+
+ /**
+ * Creates a new [PomDependency] from the given XML [Element].
+ */
+ fun fromXmlElement(node: Element, properties: Map<String, String>) : PomDependency {
+ var groupId : String? = null
+ var artifactId : String? = null
+ var version : String? = null
+ var classifier : String? = null
+ var type : String? = null
+ var scope : String? = null
+ var systemPath : String? = null
+ var optional : String? = null
+
+ for (childNode in node.children) {
+ when (childNode.name) {
+ "groupId" -> groupId = XmlUtils.resolveValue(childNode.value, properties)
+ "artifactId" -> artifactId = XmlUtils.resolveValue(childNode.value, properties)
+ "version" -> version = XmlUtils.resolveValue(childNode.value, properties)
+ "classifier" -> classifier = XmlUtils.resolveValue(childNode.value, properties)
+ "type" -> type = XmlUtils.resolveValue(childNode.value, properties)
+ "scope" -> scope = XmlUtils.resolveValue(childNode.value, properties)
+ "systemPath" -> systemPath = XmlUtils.resolveValue(childNode.value, properties)
+ "optional" -> optional = XmlUtils.resolveValue(childNode.value, properties)
+ }
+ }
+
+ return PomDependency(
+ groupId = groupId,
+ artifactId = artifactId,
+ version = version,
+ classifier = classifier,
+ type = type,
+ scope = scope,
+ systemPath = systemPath,
+ optional = optional)
+ }
+
+ }
+
+ init {
+ if (version != null) {
+ version = version!!.toLowerCase()
+ }
+ }
+
+ /**
+ * Whether this dependency should be skipped from the rewriting process
+ */
+ fun shouldSkipRewrite() : Boolean {
+ return scope != null && scope.toLowerCase() == "test"
+ }
+
+ /**
+ * Returns a new dependency created by taking all the items from the [input] dependency and then
+ * overwriting these with all of its non-null items.
+ */
+ fun rewrite(input: PomDependency) : PomDependency {
+ return PomDependency(
+ groupId = groupId ?: input.groupId,
+ artifactId = artifactId ?: input.artifactId,
+ version = version ?: input.version,
+ classifier = classifier ?: input.classifier,
+ type = type ?: input.type,
+ scope = scope ?: input.scope,
+ systemPath = systemPath ?: input.systemPath,
+ optional = optional ?: input.optional
+ )
+ }
+
+ /**
+ * Transforms the current data into XML '<dependency>' node.
+ */
+ fun toXmlElement(document: Document) : Element {
+ val node = Element("dependency")
+ node.namespace = document.rootElement.namespace
+
+ XmlUtils.addStringNodeToNode(node, "groupId", groupId)
+ XmlUtils.addStringNodeToNode(node, "artifactId", artifactId)
+ XmlUtils.addStringNodeToNode(node, "version", version)
+ XmlUtils.addStringNodeToNode(node, "classifier", classifier)
+ XmlUtils.addStringNodeToNode(node, "type", type)
+ XmlUtils.addStringNodeToNode(node, "scope", scope)
+ XmlUtils.addStringNodeToNode(node, "systemPath", systemPath)
+ XmlUtils.addStringNodeToNode(node, "optional", optional)
+
+ return node
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt
new file mode 100644
index 0000000..d5bdc3a
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.utils.Log
+import org.jdom2.Document
+import org.jdom2.Element
+
+/**
+ * Wraps a single POM XML [ArchiveFile] with parsed metadata about transformation related sections.
+ */
+class PomDocument(val file: ArchiveFile, private val document: Document) {
+
+ companion object {
+ private const val TAG = "Pom"
+
+ fun loadFrom(file: ArchiveFile) : PomDocument {
+ val document = XmlUtils.createDocumentFromByteArray(file.data)
+ val pomDoc = PomDocument(file, document)
+ pomDoc.initialize()
+ return pomDoc
+ }
+ }
+
+ val dependencies : MutableSet<PomDependency> = mutableSetOf()
+ private val properties : MutableMap<String, String> = mutableMapOf()
+ private var dependenciesGroup : Element? = null
+ private var hasChanged : Boolean = false
+
+ private fun initialize() {
+ val propertiesGroup = document.rootElement
+ .getChild("properties", document.rootElement.namespace)
+ if (propertiesGroup != null) {
+ propertiesGroup.children
+ .filterNot { it.value.isNullOrEmpty() }
+ .forEach { properties[it.name] = it.value }
+ }
+
+ dependenciesGroup = document.rootElement
+ .getChild("dependencies", document.rootElement.namespace) ?: return
+ dependenciesGroup!!.children.mapTo(dependencies) {
+ PomDependency.fromXmlElement(it, properties)
+ }
+ }
+
+ /**
+ * Validates that this document is consistent with the provided [rules].
+ *
+ * Currently it checks that all the dependencies that are going to be rewritten by the given
+ * rules satisfy the minimal version requirements defined by the rules.
+ */
+ fun validate(rules: List<PomRewriteRule>) : Boolean {
+ if (dependenciesGroup == null) {
+ // Nothing to validate as this file has no dependencies section
+ return true
+ }
+
+ return dependencies.all { dep -> rules.all { it.validateVersion(dep) } }
+ }
+
+ /**
+ * Applies the given [rules] to rewrite the POM file.
+ *
+ * Changes are not saved back until requested.
+ */
+ fun applyRules(rules: List<PomRewriteRule>) {
+ if (dependenciesGroup == null) {
+ // Nothing to transform as this file has no dependencies section
+ return
+ }
+
+ val newDependencies = mutableSetOf<PomDependency>()
+ for (dependency in dependencies) {
+ if (dependency.shouldSkipRewrite()) {
+ continue
+ }
+
+ val rule = rules.firstOrNull { it.matches(dependency) }
+ if (rule == null) {
+ // No rule to rewrite => keep it
+ newDependencies.add(dependency)
+ } else {
+ // Replace with new dependencies
+ newDependencies.addAll(rule.to.mapTo(newDependencies){ it.rewrite(dependency) })
+ }
+ }
+
+ if (newDependencies.isEmpty()) {
+ // No changes
+ return
+ }
+
+ dependenciesGroup!!.children.clear()
+ newDependencies.forEach { dependenciesGroup!!.addContent(it.toXmlElement(document)) }
+ hasChanged = true
+ }
+
+ /**
+ * Saves any current pending changes back to the file if needed.
+ */
+ fun saveBackToFileIfNeeded() {
+ if (!hasChanged) {
+ return
+ }
+
+ file.data = XmlUtils.convertDocumentToByteArray(document)
+ }
+
+ /**
+ * Logs the information about the current file using info level.
+ */
+ fun logDocumentDetails() {
+ Log.i(TAG, "POM file at: '%s'", file.relativePath)
+ for ((groupId, artifactId, version) in dependencies) {
+ Log.i(TAG, "- Dep: %s:%s:%s", groupId, artifactId, version)
+ }
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRule.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRule.kt
new file mode 100644
index 0000000..070a640
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRule.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.utils.Log
+import com.google.gson.annotations.SerializedName
+
+/**
+ * Rule that defines how to rewrite a dependency element in a POM file.
+ *
+ * Any dependency that is matched against [from] should be rewritten to list of the dependencies
+ * defined in [to].
+ */
+data class PomRewriteRule(val from: PomDependency, val to: List<PomDependency>) {
+
+ companion object {
+ val TAG : String = "PomRule"
+ }
+
+ /**
+ * Validates that the given [input] dependency has a valid version.
+ */
+ fun validateVersion(input: PomDependency, document: PomDocument? = null) : Boolean {
+ if (from.version == null || input.version == null) {
+ return true
+ }
+
+ if (!matches(input)) {
+ return true
+ }
+
+ if (!areVersionsMatching(from.version!!, input.version!!)) {
+ Log.e(TAG, "Version mismatch! Expected version '%s' but found version '%s' for " +
+ "'%s:%s' in '%s' file.", from.version, input.version, input.groupId,
+ input.artifactId, document?.file?.relativePath)
+ return false
+ }
+
+ return true
+ }
+
+ /**
+ * Checks if the given [version] is supported to be rewritten with a rule having [ourVersion].
+ *
+ * Version entry can be actually quite complicated, see the full documentation at:
+ * https://maven.apache.org/pom.html#Dependencies
+ */
+ private fun areVersionsMatching(ourVersion: String, version: String) : Boolean {
+ if (version == "latest" || version == "release") {
+ return true
+ }
+
+ if (version.endsWith(",)") || version.endsWith(",]")) {
+ return true
+ }
+
+ if (version.endsWith("$ourVersion]")) {
+ return true
+ }
+
+ return ourVersion == version
+ }
+
+ fun matches(input: PomDependency) : Boolean {
+ return input.artifactId == from.artifactId && input.groupId == from.groupId
+ }
+
+ /** Returns JSON data model of this class */
+ fun toJson() : PomRewriteRule.JsonData {
+ return PomRewriteRule.JsonData(from, to)
+ }
+
+
+ /**
+ * JSON data model for [PomRewriteRule].
+ */
+ data class JsonData(
+ @SerializedName("from")
+ val from: PomDependency,
+ @SerializedName("to")
+ val to: List<PomDependency>) {
+
+ /** Creates instance of [PomRewriteRule] */
+ fun toRule() : PomRewriteRule {
+ return PomRewriteRule(from, to.filterNotNull())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomScanner.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomScanner.kt
new file mode 100644
index 0000000..e9cc511
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomScanner.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.archive.ArchiveItemVisitor
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.utils.Log
+
+/**
+ * Helper to scan [Archive]s to find their POM files.
+ */
+class PomScanner(private val config: Config) {
+
+ companion object {
+ private const val TAG = "PomScanner"
+ }
+
+ private val pomFilesInternal = mutableListOf<PomDocument>()
+
+ private var validationFailuresCount = 0
+
+ val pomFiles : List<PomDocument> = pomFilesInternal
+
+ fun wasErrorFound() = validationFailuresCount > 0
+
+ /**
+ * Scans the given [archive] for a POM file
+ *
+ * @return null if POM file was not found
+ */
+ fun scanArchiveForPomFile(archive: Archive) : PomDocument? {
+ val session = PomScannerSession()
+ archive.accept(session)
+
+ if (session.pomFile == null) {
+ return null
+ }
+ val pomFile = session.pomFile!!
+
+ pomFile.logDocumentDetails()
+
+ if (!pomFile.validate(config.pomRewriteRules)) {
+ Log.e(TAG, "Version mismatch!")
+ validationFailuresCount++
+ }
+
+ pomFilesInternal.add(session.pomFile!!)
+
+ return session.pomFile
+ }
+
+
+ private class PomScannerSession : ArchiveItemVisitor {
+
+ var pomFile : PomDocument? = null
+
+ override fun visit(archive: Archive) {
+ for (archiveItem in archive.files) {
+ if (pomFile != null) {
+ break
+ }
+ archiveItem.accept(this)
+ }
+ }
+
+ override fun visit(archiveFile: ArchiveFile) {
+ if (archiveFile.isPomFile()) {
+ pomFile = PomDocument.loadFrom(archiveFile)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/XmlUtils.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/XmlUtils.kt
new file mode 100644
index 0000000..67b7a3d
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/XmlUtils.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.utils.Log
+import org.jdom2.Document
+import org.jdom2.Element
+import org.jdom2.input.SAXBuilder
+import org.jdom2.output.Format
+import org.jdom2.output.XMLOutputter
+import java.io.ByteArrayOutputStream
+import java.util.regex.Pattern
+
+/**
+ * Utilities for handling XML documents.
+ */
+class XmlUtils {
+
+ companion object {
+
+ private val variablePattern = Pattern.compile("\\$\\{([^}]*)}")
+
+ /** Saves the given [Document] to a new byte array */
+ fun convertDocumentToByteArray(document : Document) : ByteArray {
+ val xmlOutput = XMLOutputter()
+ ByteArrayOutputStream().use {
+ xmlOutput.format = Format.getPrettyFormat()
+ xmlOutput.output(document, it)
+ return it.toByteArray()
+ }
+ }
+
+ /** Creates a new [Document] from the given [ByteArray] */
+ fun createDocumentFromByteArray(data: ByteArray) : Document {
+ val builder = SAXBuilder()
+ data.inputStream().use {
+ return builder.build(it)
+ }
+ }
+
+ /**
+ * Creates a new XML element with the given [id] and text given in [value] and puts it under
+ * the given [parent]. Nothing is created if the [value] argument is null or empty.
+ */
+ fun addStringNodeToNode(parent: Element, id: String, value: String?) {
+ if (value.isNullOrEmpty()) {
+ return
+ }
+
+ val element = Element(id)
+ element.text = value
+ element.namespace = parent.namespace
+ parent.children.add(element)
+ }
+
+
+ fun resolveValue(value: String?, properties: Map<String, String>) : String? {
+ if (value == null) {
+ return null
+ }
+
+ val matcher = variablePattern.matcher(value)
+ if (matcher.matches()) {
+ val variableName = matcher.group(1)
+ val varValue = properties[variableName]
+ if (varValue == null) {
+ Log.e("TAG", "Failed to resolve variable '%s'", value)
+ return value
+ }
+ return varValue
+ }
+
+ return value
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassFilterParser.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassFilterParser.kt
new file mode 100644
index 0000000..c431572
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassFilterParser.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.transform.proguard.patterns.GroupsReplacer
+import android.support.tools.jetifier.core.transform.proguard.patterns.PatternHelper
+import java.util.regex.Pattern
+
+/**
+ * Parses and rewrites ProGuard rules that contain class filters. See ProGuard documentation
+ * https://www.guardsquare.com/en/proguard/manual/usage#filters
+ */
+class ProGuardClassFilterParser(private val mapper : ProGuardTypesMapper) {
+
+ companion object {
+ private const val RULES = "(adaptclassstrings|dontnote|dontwarn)"
+ }
+
+ val replacer = GroupsReplacer(
+ pattern = PatternHelper.build("^ *-$RULES ⦅[^-]+ï½ *$", Pattern.MULTILINE),
+ groupsMap = listOf(
+ { filter : String -> rewriteClassFilter(filter) }
+ )
+ )
+
+ private fun rewriteClassFilter(classFilter: String) : String {
+ return classFilter
+ .splitToSequence(",")
+ .filterNotNull()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .map { replaceTypeInClassFilter(it) }
+ .joinToString(separator = ", ")
+ }
+
+ private fun replaceTypeInClassFilter(type: String) : String {
+ if (!type.startsWith('!')) {
+ return mapper.replaceType(type)
+ }
+
+ val withoutNegation = type.substring(1, type.length)
+ return '!' + mapper.replaceType(withoutNegation)
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassSpecParser.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassSpecParser.kt
new file mode 100644
index 0000000..933ff08
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardClassSpecParser.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.transform.proguard.patterns.GroupsReplacer
+import android.support.tools.jetifier.core.transform.proguard.patterns.PatternHelper
+
+/**
+ * Parses and rewrites ProGuard rules that contain class specification. See ProGuard documentation
+ * https://www.guardsquare.com/en/proguard/manual/usage#classspecification
+ */
+class ProGuardClassSpecParser(private val mapper : ProGuardTypesMapper) {
+
+ companion object {
+ private const val RULES = "(keep[a-z]*|whyareyoukeeping|assumenosideeffects)"
+ private const val RULES_MODIFIERS =
+ "(includedescriptorclasses|allowshrinking|allowoptimization|allowobfuscation)"
+
+ private const val CLASS_NAME = "[\\w.$?*_%]+"
+ private const val CLASS_MODIFIERS = "[!]?(public|final|abstract)"
+ private const val CLASS_TYPES = "[!]?(interface|class|enum)"
+
+ private const val ANNOTATION_TYPE = CLASS_NAME
+
+ private const val FIELD_NAME = "[\\w?*_%]+"
+ private const val FIELD_TYPE = CLASS_NAME
+ private const val FIELD_MODIFIERS =
+ "[!]?(public|private|protected|static|volatile|transient)"
+
+ private const val METHOD_MODIFIERS =
+ "[!]?(public|private|protected|static|synchronized|native|abstract|strictfp)"
+ private const val RETURN_TYPE_NAME = CLASS_NAME
+ private const val METHOD_NAME = "[\\w?*_]+"
+ private const val ARGS = "[^)]*"
+ }
+
+ val replacer = GroupsReplacer(
+ pattern = PatternHelper.build(
+ "-$RULES ($RULES_MODIFIERS )*(@⦅$ANNOTATION_TYPEï½ )?($CLASS_MODIFIERS )*$CLASS_TYPES " +
+ "⦅$CLASS_NAMEï½ ( (extends|implements) ⦅$CLASS_NAMEï½ )?+ *( *\\{⦅[^}]*ï½ \\} *)?+"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) },
+ { className : String -> mapper.replaceType(className) },
+ { className2 : String -> mapper.replaceType(className2) },
+ { bodyGroup : String -> rewriteBodyGroup(bodyGroup) }
+ )
+ )
+
+ private val bodyReplacers = listOf(
+ // [@annotation] [[!]public|private|etc...] <fields>;
+ GroupsReplacer(
+ pattern = PatternHelper.build(
+ "^ *(@⦅$ANNOTATION_TYPEï½ )?($FIELD_MODIFIERS )*<fields> *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) }
+ )),
+
+ // [@annotation] [[!]public|private|etc...] fieldType fieldName;
+ GroupsReplacer(
+ pattern = PatternHelper.build(
+ "^ *(@⦅$ANNOTATION_TYPEï½ )?($FIELD_MODIFIERS )*(⦅$FIELD_TYPEï½ $FIELD_NAME) *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) },
+ { fieldType : String -> mapper.replaceType(fieldType) }
+ )),
+
+ // [@annotation] [[!]public|private|etc...] <methods>;
+ GroupsReplacer(
+ pattern = PatternHelper.build(
+ "^ *(@⦅$ANNOTATION_TYPEï½ )?($METHOD_MODIFIERS )*<methods> *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) }
+ )),
+
+ // [@annotation] [[!]public|private|etc...] className(argumentType,...));
+ GroupsReplacer(
+ pattern = PatternHelper.build(
+ "^ *(@⦅$ANNOTATION_TYPEï½ )?($METHOD_MODIFIERS )*⦅$CLASS_NAMEï½ *\\(⦅$ARGSï½ \\) *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) },
+ { className : String -> mapper.replaceType(className) },
+ { argsType : String -> mapper.replaceMethodArgs(argsType) }
+ )
+ ),
+
+ // [@annotation] [[!]public|private|etc...] <init>(argumentType,...));
+ GroupsReplacer(
+ pattern = PatternHelper.build(
+ "^ *(@⦅$ANNOTATION_TYPEï½ )?($METHOD_MODIFIERS )*<init> *\\(⦅$ARGSï½ \\) *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) },
+ { argsType : String -> mapper.replaceMethodArgs(argsType) }
+ )),
+
+ // [@annotation] [[!]public|private|etc...] returnType methodName(argumentType,...));
+ GroupsReplacer(
+ pattern = PatternHelper.build("^ *(@⦅$ANNOTATION_TYPEï½ )?($METHOD_MODIFIERS )*" +
+ "⦅$RETURN_TYPE_NAMEï½ $METHOD_NAME *\\(⦅$ARGSï½ \\) *$"),
+ groupsMap = listOf(
+ { annotation : String -> mapper.replaceType(annotation) },
+ { returnType : String -> mapper.replaceType(returnType) },
+ { argsType : String -> mapper.replaceMethodArgs(argsType) }
+ ))
+ )
+
+ private fun rewriteBodyGroup(bodyGroup: String) : String {
+ if (bodyGroup == "*" || bodyGroup == "**") {
+ return bodyGroup
+ }
+
+ return bodyGroup
+ .split(';')
+ .map {
+ for (replacer in bodyReplacers) {
+ val matcher = replacer.pattern.matcher(it)
+ if (matcher.matches()) {
+ return@map replacer.runReplacements(matcher)
+ }
+ }
+ return@map it
+ }
+ .joinToString(";")
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTransformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTransformer.kt
new file mode 100644
index 0000000..423bf05
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTransformer.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.Transformer
+import android.support.tools.jetifier.core.transform.proguard.patterns.ReplacersRunner
+import java.nio.charset.StandardCharsets
+
+/**
+ * The [Transformer] responsible for ProGuard files refactoring.
+ */
+class ProGuardTransformer internal constructor(context: TransformationContext) : Transformer {
+
+ private val mapper = ProGuardTypesMapper(context)
+
+ val replacer = ReplacersRunner(listOf(
+ ProGuardClassSpecParser(mapper).replacer,
+ ProGuardClassFilterParser(mapper).replacer
+ ))
+
+ override fun canTransform(file: ArchiveFile): Boolean {
+ return file.isProGuardFile()
+ }
+
+ override fun runTransform(file: ArchiveFile) {
+ val sb = StringBuilder(file.data.toString(StandardCharsets.UTF_8))
+ val result = replacer.applyReplacers(sb.toString())
+ file.data = result.toByteArray()
+ }
+}
+
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardType.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardType.kt
new file mode 100644
index 0000000..be15fbf
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardType.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.rules.JavaType
+
+/**
+ * Represents a type reference in ProGuard file. This type is similar to the regular java type but
+ * can also contain wildcards (*,**,?).
+ */
+data class ProGuardType(val value: String) {
+
+ init {
+ if (value.contains('.')) {
+ throw IllegalArgumentException("The type does not support '.' as package separator!")
+ }
+ }
+
+ companion object {
+ /** Creates the type reference from notation where packages are separated using '.' */
+ fun fromDotNotation(type: String) : ProGuardType {
+ return ProGuardType(type.replace('.', '/'))
+ }
+ }
+
+ /**
+ * Whether the type reference is trivial such as "*".
+ */
+ fun isTrivial() = value == "*" || value == "**" || value == "***" || value == "%"
+
+ fun toJavaType() : JavaType? {
+ if (value.contains('*') || value.contains('?')) {
+ return null
+ }
+ return JavaType(value)
+ }
+
+ /** Returns the type reference as a string where packages are separated using '.' */
+ fun toDotNotation() : String {
+ return value.replace('/', '.')
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMap.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMap.kt
new file mode 100644
index 0000000..03d6282
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMap.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+/**
+ * Contains custom mappings to map support library types referenced in ProGuard to new ones.
+ */
+data class ProGuardTypesMap(val rules: Map<ProGuardType, ProGuardType>) {
+
+ companion object {
+ val EMPTY = ProGuardTypesMap(emptyMap())
+ }
+
+ /** Returns JSON data model of this class */
+ fun toJson() : JsonData {
+ return JsonData(rules.map { it.key.value to it.value.value }.toMap())
+ }
+
+ /**
+ * JSON data model for [ProGuardTypesMap].
+ */
+ data class JsonData(val rules: Map<String, String>) {
+
+ /** Creates instance of [ProGuardTypesMap] */
+ fun toMappings() : ProGuardTypesMap {
+ return ProGuardTypesMap(rules.map { ProGuardType(it.key) to ProGuardType(it.value) }.toMap())
+ }
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapper.kt
new file mode 100644
index 0000000..28195a3
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapper.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.utils.Log
+
+/**
+ * Maps ProGuard types using [TypesMap] and [ProGuardTypesMap].
+ */
+class ProGuardTypesMapper(private val context: TransformationContext) {
+
+ companion object {
+ const val TAG = "ProGuardTypesMapper"
+ }
+
+ private val config = context.config
+
+ /**
+ * Replaces the given ProGuard type that was parsed from the ProGuard file (thus having '.' as
+ * a separator.
+ */
+ fun replaceType(typeToReplace: String) : String {
+ val type = ProGuardType.fromDotNotation(typeToReplace)
+ if (type.isTrivial()) {
+ return typeToReplace
+ }
+
+ val javaType = type.toJavaType()
+ if (javaType != null) {
+ // We are dealing with an explicit type definition
+ if (!context.isEligibleForRewrite(javaType)) {
+ return typeToReplace
+ }
+
+ val result = config.typesMap.types[javaType]
+ if (result == null) {
+ context.reportNoProGuardMappingFoundFailure()
+ Log.e(TAG, "No mapping for: " + type)
+ return typeToReplace
+ }
+
+ Log.i(TAG, " map: %s -> %s", type, result)
+ return result.toDotNotation()
+ }
+
+ // Type contains wildcards - try custom rules map
+ val result = config.proGuardMap.rules[type]
+ if (result != null) {
+ Log.i(TAG, " map: %s -> %s", type, result)
+ return result.toDotNotation()
+ }
+
+ // Report error only when we are sure
+ if (context.isEligibleForRewrite(type)) {
+ context.reportNoProGuardMappingFoundFailure()
+ Log.e(TAG, "No mapping for: " + type)
+ }
+ return typeToReplace
+ }
+
+ /**
+ * Replaces the given arguments list used in a ProGuard method rule. Argument must be separated
+ * with ','. The method also accepts '...' symbol as defined in the spec.
+ */
+ fun replaceMethodArgs(argsTypes: String) : String {
+ if (argsTypes.isEmpty() || argsTypes == "...") {
+ return argsTypes
+ }
+
+ return argsTypes
+ .splitToSequence(",")
+ .filterNotNull()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .map { replaceType(it) }
+ .joinToString(separator = ", ")
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/GroupsReplacer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/GroupsReplacer.kt
new file mode 100644
index 0000000..6213a55
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/GroupsReplacer.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard.patterns
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * Applies replacements on a matched string using the given [pattern] and its groups. Each group is
+ * mapped using a lambda from [groupsMap].
+ */
+class GroupsReplacer(val pattern: Pattern,
+ private val groupsMap: List<(String) -> String>) {
+
+ /**
+ * Takes the given [matcher] and replace its matched groups using mapping functions given in
+ * [groupsMap].
+ */
+ fun runReplacements(matcher: Matcher) : String {
+ var result = matcher.group(0)
+
+ // We go intentionally backwards to replace using indexes
+ for (i in groupsMap.size - 1 downTo 0) {
+ val groupVal = matcher.group(i + 1) ?: continue
+ val localStart = matcher.start(i + 1) - matcher.start()
+ val localEnd = matcher.end(i + 1) - matcher.start()
+
+ result = result.replaceRange(
+ startIndex = localStart,
+ endIndex = localEnd,
+ replacement = groupsMap[i].invoke(groupVal))
+ }
+ return result
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/PatternHelper.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/PatternHelper.kt
new file mode 100644
index 0000000..3171185
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/PatternHelper.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard.patterns
+
+import java.util.regex.Pattern
+
+/**
+ * Helps to build regular expression [Pattern]s defined with less verbose syntax.
+ *
+ * You can use following shortcuts:
+ * 'ï½Ÿï½ ' - denotes a capturing group (normally '()' is capturing group)
+ * '()' - denotes non-capturing group (normally (?:) is non-capturing group)
+ * ' ' - denotes a whitespace characters (at least one)
+ * ' *' - denotes a whitespace characters (any)
+ * ';' - denotes ' *;'
+ */
+object PatternHelper {
+
+ private val rewrites = listOf(
+ " *" to "[\\s]*", // Optional space
+ " " to "[\\s]+", // Space
+ "⦅" to "(", // Capturing group start
+ "ï½ " to ")", // Capturing group end
+ ";" to "[\\s]*;" // Allow spaces in front of ';'
+ )
+
+ /**
+ * Transforms the given [toReplace] according to the rules defined in documentation of this
+ * class and compiles it to a [Pattern].
+ */
+ fun build(toReplace: String, flags : Int = 0) : Pattern {
+ var result = toReplace
+ result = result.replace("(?<!\\\\)\\(".toRegex(), "(?:")
+ rewrites.forEach { result = result.replace(it.first, it.second) }
+ return Pattern.compile(result, flags)
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/ReplacersRunner.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/ReplacersRunner.kt
new file mode 100644
index 0000000..54501f9
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/proguard/patterns/ReplacersRunner.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard.patterns
+
+/**
+ * Runs multiple [GroupsReplacer]s on given strings.
+ */
+class ReplacersRunner(val replacers: List<GroupsReplacer>) {
+
+ /**
+ * Runs all the [GroupsReplacer]s on the given [input].
+ *
+ * The replacers have to be distinct as this method can't guarantee that output of one replacer
+ * won't be matched by another replacer.
+ */
+ fun applyReplacers(input : String) : String {
+ val sb = StringBuilder()
+ var lastSeenChar = 0
+ var processedInput = input
+
+ for (replacer in replacers) {
+ val matcher = replacer.pattern.matcher(processedInput)
+
+ while (matcher.find()) {
+ if (lastSeenChar < matcher.start()) {
+ sb.append(input, lastSeenChar, matcher.start())
+ }
+
+ val result = replacer.runReplacements(matcher)
+ sb.append(result)
+ lastSeenChar = matcher.end()
+ }
+
+ if (lastSeenChar == 0) {
+ continue
+ }
+
+ if (lastSeenChar <= processedInput.length - 1) {
+ sb.append(processedInput, lastSeenChar, processedInput.length)
+ }
+
+ lastSeenChar = 0
+ processedInput = sb.toString()
+ sb.setLength(0)
+ }
+
+ return processedInput
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt
new file mode 100644
index 0000000..0a29828
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.resource
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.rules.JavaTypeXmlRef
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.Transformer
+import android.support.tools.jetifier.core.utils.Log
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
+import java.util.regex.Pattern
+import javax.xml.stream.XMLInputFactory
+
+/**
+ * Transformer for XML resource files.
+ *
+ * Searches for any java type reference that is pointing to the support library and rewrites it
+ * using the available mappings from [TypesMap].
+ */
+class XmlResourcesTransformer internal constructor(private val context: TransformationContext)
+ : Transformer {
+
+ companion object {
+ const val TAG = "XmlResourcesTransformer"
+
+ const val PATTERN_TYPE_GROUP = 1
+ }
+
+ /**
+ * List of regular expression patterns used to find support library references in XML files.
+ *
+ * Matches xml tags in form of:
+ * 1. '<(/)prefix(SOMETHING)'.
+ * 2. <view ... class="prefix(SOMETHING)" ...>
+ *
+ * Note that this can also rewrite commented blocks of XML. But on a library level we don't care
+ * much about comments.
+ */
+ private val patterns = listOf(
+ Pattern.compile("</?([a-zA-Z0-9.]+)"),
+ Pattern.compile("<view[^>]*class=\"([a-zA-Z0-9.\$_]+)\"[^>]*>")
+ )
+
+ private val typesMap = context.config.typesMap
+
+ override fun canTransform(file: ArchiveFile) = file.isXmlFile() && !file.isPomFile()
+
+ override fun runTransform(file: ArchiveFile) {
+ file.data = transform(file.data)
+ }
+
+ fun transform(data: ByteArray) : ByteArray {
+ var changesDone = false
+
+ val charset = getCharset(data)
+ val sb = StringBuilder(data.toString(charset))
+ for (pattern in patterns) {
+ var matcher = pattern.matcher(sb)
+ while (matcher.find()) {
+ val typeToReplace = JavaTypeXmlRef(matcher.group(PATTERN_TYPE_GROUP))
+ val result = rewriteType(typeToReplace)
+ if (result == typeToReplace) {
+ continue
+ }
+ sb.replace(matcher.start(PATTERN_TYPE_GROUP), matcher.end(PATTERN_TYPE_GROUP),
+ result.fullName)
+ changesDone = true
+ matcher = pattern.matcher(sb)
+ }
+ }
+
+ if (changesDone) {
+ return sb.toString().toByteArray(charset)
+ }
+
+ return data
+ }
+
+ fun getCharset(data: ByteArray) : Charset {
+ data.inputStream().use {
+ val xmlReader = XMLInputFactory.newInstance().createXMLStreamReader(it)
+
+ xmlReader.encoding ?: return StandardCharsets.UTF_8 // Encoding was not detected
+
+ val result = Charset.forName(xmlReader.encoding)
+ if (result == null) {
+ Log.e(TAG, "Failed to find charset for encoding '%s'", xmlReader.encoding)
+ return StandardCharsets.UTF_8
+ }
+ return result
+ }
+ }
+
+ fun rewriteType(type: JavaTypeXmlRef): JavaTypeXmlRef {
+ val javaType = type.toJavaType()
+ if (!context.isEligibleForRewrite(javaType)) {
+ return type
+ }
+
+ val result = typesMap.types[javaType]
+ if (result != null) {
+ Log.i(TAG, " map: %s -> %s", type, result)
+ return JavaTypeXmlRef(result)
+ }
+
+ context.reportNoMappingFoundFailure()
+ Log.e(TAG, "No mapping for: " + type)
+ return type
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/Log.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/Log.kt
new file mode 100644
index 0000000..902dea4
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/Log.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.utils
+
+object Log {
+
+ var currentLevel : LogLevel = LogLevel.INFO
+
+ var logConsumer : LogConsumer = StdOutLogConsumer()
+
+ fun setLevel(level: String?) {
+ currentLevel = when (level) {
+ "debug" -> LogLevel.DEBUG
+ "verbose" -> LogLevel.VERBOSE
+ else -> LogLevel.INFO
+ }
+
+ }
+
+ fun e(tag: String, message: String, vararg args: Any?) {
+ if (currentLevel >= LogLevel.ERROR) {
+ logConsumer.error("[$tag] $message".format(*args))
+ }
+ }
+
+ fun d(tag: String, message: String, vararg args: Any?) {
+ if (currentLevel >= LogLevel.DEBUG) {
+ logConsumer.debug("[$tag] $message".format(*args))
+ }
+ }
+
+ fun i(tag: String, message: String, vararg args: Any?) {
+ if (currentLevel >= LogLevel.INFO) {
+ logConsumer.info("[$tag] $message".format(*args))
+ }
+ }
+
+ fun v(tag: String, message: String, vararg args: Any?) {
+ if (currentLevel >= LogLevel.VERBOSE) {
+ logConsumer.verbose("[$tag] $message".format(*args))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogConsumer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogConsumer.kt
new file mode 100644
index 0000000..ddebd25
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogConsumer.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.utils
+
+/**
+ * Interface to plug custom logs consumers to [Log].
+ */
+interface LogConsumer {
+
+ fun error(message: String)
+
+ fun info(message: String)
+
+ fun verbose(message: String)
+
+ fun debug(message: String)
+
+}
+
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogLevel.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogLevel.kt
new file mode 100644
index 0000000..f46b8f6
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/LogLevel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.utils
+
+enum class LogLevel(val priority : Int) {
+ ERROR(0),
+ INFO(1),
+ VERBOSE(2),
+ DEBUG(3),
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/StdOutLogConsumer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/StdOutLogConsumer.kt
new file mode 100644
index 0000000..7cfd25e
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/utils/StdOutLogConsumer.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.utils
+
+/**
+ * Prints logs to the standard output.
+ */
+class StdOutLogConsumer : LogConsumer {
+
+ override fun error(message: String) {
+ println("ERROR: $message")
+ }
+
+ override fun info(message: String) {
+ println("INFO: $message")
+ }
+
+ override fun verbose(message: String) {
+ println("VERBOSE: $message")
+ }
+
+ override fun debug(message: String) {
+ println("DEBUG: $message")
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/resources/default.config b/jetifier/jetifier/core/src/main/resources/default.config
new file mode 100644
index 0000000..27b2374
--- /dev/null
+++ b/jetifier/jetifier/core/src/main/resources/default.config
@@ -0,0 +1,42 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+{
+ # Skip packages that don't match the following regex
+ restrictToPackagePrefixes: [
+ "android/support/"
+ ],
+ rules: [
+ # preferences.v7
+ {
+ # Take any field starting with 'dialog' prefix in R file and move it to dialogs.R
+ from: "android/support/v7/preferences/R$(.*)",
+ to: "android/jetpack/prefs/dialogs/R${0}",
+ fieldSelectors: ["dialog_(.*)"]
+ },
+ {
+ from: "android/support/v7/preferences/DialogPreference",
+ to: "android/jetpack/prefs/dialogs/DialogPreference"
+ },
+ {
+ from: "android/support/v7/preferences/(.*)",
+ to: "android/jetpack/prefs/main/{0}"
+ },
+ # preferences.v14
+ {
+ from: "android/support/v14/preferences/(.*)",
+ to: "android/jetpack/prefs/main/{0}"
+ },
+ ]
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/config/ConfigParserTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/config/ConfigParserTest.kt
new file mode 100644
index 0000000..4a03ef3
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/config/ConfigParserTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.config
+
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class ConfigParserTest {
+
+ @Test fun parseConfig_validInput() {
+ val confStr =
+ "{\n" +
+ " restrictToPackagePrefixes: [\"android/support/\"],\n" +
+ " # Sample comment \n" +
+ " rules: [\n" +
+ " {\n" +
+ " from: \"android/support/v14/preferences/(.*)\",\n" +
+ " to: \"android/jetpack/prefs/main/{0}\"\n" +
+ " },\n" +
+ " {\n" +
+ " from: \"android/support/v14/preferences/(.*)\",\n" +
+ " to: \"android/jetpack/prefs/main/{0}\",\n" +
+ " fieldSelectors: [\"dialog_(.*)\"]\n" +
+ " }\n" +
+ " ],\n" +
+ " pomRules: [\n" +
+ " {\n" +
+ " from: {groupId: \"g\", artifactId: \"a\", version: \"1.0\"},\n" +
+ " to: [\n" +
+ " {groupId: \"g\", artifactId: \"a\", version: \"2.0\"} \n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}"
+
+ val config = ConfigParser.parseFromString(confStr)
+
+ Truth.assertThat(config).isNotNull()
+ Truth.assertThat(config!!.restrictToPackagePrefixes[0]).isEqualTo("android/support/")
+ Truth.assertThat(config.rewriteRules.size).isEqualTo(2)
+ }
+}
+
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/map/MapGenerationTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/map/MapGenerationTest.kt
new file mode 100644
index 0000000..e7f8570
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/map/MapGenerationTest.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.map
+
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.rules.RewriteRule
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.common.truth.Truth
+import org.junit.Test
+
+
+class MapGenerationTest {
+
+ @Test fun fromOneType_toOneType() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenTypes(
+ JavaType("android/support/v7/pref/Preference")
+ )
+ .mapInto(
+ types = mapOf(
+ "android/support/v7/pref/Preference" to "android/test/pref/Preference"
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun fromTwoTypes_toOneType_prefixRespected() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}"),
+ RewriteRule("android/support/v14/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/v7/"
+ )
+ .forGivenTypes(
+ JavaType("android/support/v7/pref/Preference"),
+ JavaType("android/support/v14/pref/PreferenceDialog")
+ )
+ .mapInto(
+ types = mapOf(
+ "android/support/v7/pref/Preference" to "android/test/pref/Preference"
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun fromTwoTypes_toTwoTypes_distinctRules() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}"),
+ RewriteRule("android/support/v14/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/v7/",
+ "android/support/v14/"
+ )
+ .forGivenTypes(
+ JavaType("android/support/v7/pref/Preference"),
+ JavaType("android/support/v14/pref/PreferenceDialog")
+ )
+ .mapInto(
+ types = mapOf(
+ "android/support/v7/pref/Preference" to "android/test/pref/Preference",
+ "android/support/v14/pref/PreferenceDialog" to "android/test/pref/PreferenceDialog"
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun fromTwoTypes_toTwoTypes_respectsOrder() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v14/(.*)", "android/test/{0}"),
+ RewriteRule("android/support/(.*)", "android/fallback/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenTypes(
+ JavaType("android/support/v7/pref/Preference"),
+ JavaType("android/support/v14/pref/PreferenceDialog")
+ )
+ .mapInto(
+ types = mapOf(
+ "android/support/v7/pref/Preference" to "android/fallback/v7/pref/Preference",
+ "android/support/v14/pref/PreferenceDialog" to "android/test/pref/PreferenceDialog"
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun mapTwoFields_usingOneTypeRule() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenFields(
+ JavaField("android/support/v7/pref/Preference", "count"),
+ JavaField("android/support/v7/pref/Preference", "min")
+ )
+ .mapInto(
+ types = mapOf(
+ ),
+ fields = mapOf(
+ "android/support/v7/pref/Preference" to mapOf(
+ "android/test/pref/Preference" to listOf(
+ "count",
+ "min"
+ )
+ )
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun mapFieldInInnerClass_usingOneTypeRule() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenFields(
+ JavaField("android/support/v7/pref/R\$attr", "border")
+ )
+ .mapInto(
+ types = mapOf(
+ ),
+ fields = mapOf(
+ "android/support/v7/pref/R\$attr" to mapOf(
+ "android/test/pref/R\$attr" to listOf(
+ "border"
+ )
+ )
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun mapPrivateFields_shouldIgnore() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}")
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenFields(
+ JavaField("android/support/v7/pref/Preference", "mCount"),
+ JavaField("android/support/v7/pref/Preference", "this$0")
+ )
+ .mapInto(
+ types = mapOf(
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsComplete()
+ }
+
+ @Test fun mapType_usingFieldSelector_shouldNotApply() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}", listOf("count"))
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenTypes(
+ JavaType("android/support/v7/pref/Preference")
+ )
+ .mapInto(
+ types = mapOf(
+ "android/support/v7/pref/Preference" to "android/support/v7/pref/Preference"
+ ),
+ fields = mapOf(
+ )
+ )
+ .andIsNotComplete()
+ }
+
+ @Test fun mapField_noApplicableRule() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}", listOf("count2"))
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenFields(
+ JavaField("android/support/v7/pref/Preference", "count")
+ )
+ .mapInto(
+ types = mapOf(
+ ),
+ fields = mapOf(
+ "android/support/v7/pref/Preference" to mapOf(
+ "android/support/v7/pref/Preference" to listOf(
+ "count"
+ )
+ )
+ )
+ )
+ .andIsNotComplete()
+ }
+
+ @Test fun mapTwoFields_usingTwoFieldsSelectors() {
+ ScanTester
+ .testThatRules(
+ RewriteRule("android/support/v7/(.*)", "android/test/{0}", listOf("count")),
+ RewriteRule("android/support/v7/(.*)", "android/test2/{0}", listOf("size"))
+ )
+ .withAllowedPrefixes(
+ "android/support/"
+ )
+ .forGivenFields(
+ JavaField("android/support/v7/pref/Preference", "count"),
+ JavaField("android/support/v7/pref/Preference", "size")
+ )
+ .mapInto(
+ types = mapOf(
+ ),
+ fields = mapOf(
+ "android/support/v7/pref/Preference" to mapOf(
+ "android/test/pref/Preference" to listOf(
+ "count"
+ ),
+ "android/test2/pref/Preference" to listOf(
+ "size"
+ )
+ )
+ )
+ )
+ .andIsComplete()
+ }
+
+
+ object ScanTester {
+
+ fun testThatRules(vararg rules: RewriteRule) = Step1(rules.toList())
+
+
+ class Step1(val rules: List<RewriteRule>) {
+
+ fun withAllowedPrefixes(vararg prefixes: String) = Step2(rules, prefixes.toList())
+
+
+ class Step2(val rules: List<RewriteRule>, val prefixes: List<String>) {
+
+ private val allTypes: MutableList<JavaType> = mutableListOf()
+ private val allFields: MutableList<JavaField> = mutableListOf()
+ private var wasMapIncomplete = false
+
+
+ fun forGivenTypes(vararg types: JavaType) : Step2 {
+ allTypes.addAll(types)
+ return this
+ }
+
+ fun forGivenFields(vararg fields: JavaField) : Step2 {
+ allFields.addAll(fields)
+ return this
+ }
+
+ fun mapInto(types: Map<String, String>,
+ fields: Map<String, Map<String, List<String>>>) : Step2 {
+ val config = Config(
+ restrictToPackagePrefixes = prefixes,
+ rewriteRules = rules,
+ pomRewriteRules = emptyList(),
+ typesMap = TypesMap.EMPTY,
+ proGuardMap = ProGuardTypesMap.EMPTY)
+ val scanner = MapGeneratorRemapper(config)
+
+ allTypes.forEach { scanner.rewriteType(it) }
+ allFields.forEach { scanner.rewriteField(it) }
+
+ val typesMap = scanner.createTypesMap().toJson()
+ wasMapIncomplete = scanner.isMapNotComplete
+
+ Truth.assertThat(typesMap.types).containsExactlyEntriesIn(types)
+ Truth.assertThat(typesMap.fields).containsExactlyEntriesIn(fields)
+ return this
+ }
+
+ fun andIsNotComplete() {
+ Truth.assertThat(wasMapIncomplete).isTrue()
+ }
+
+ fun andIsComplete() {
+ Truth.assertThat(wasMapIncomplete).isFalse()
+ }
+ }
+
+ }
+
+ }
+}
+
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/RewriteRuleTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/RewriteRuleTest.kt
new file mode 100644
index 0000000..ca6288d
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/RewriteRuleTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform
+
+import android.support.tools.jetifier.core.rules.JavaField
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.rules.RewriteRule
+import com.google.common.truth.Truth
+import org.junit.Test
+
+
+class RewriteRuleTest {
+
+ @Test fun noRegEx_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B", "A/C")
+ .rewritesType("A/B")
+ .into("A/C")
+ }
+
+ @Test fun noRegEx_underscore_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B_B", "A/C")
+ .rewritesType("A/B_B")
+ .into("A/C")
+ }
+
+ @Test fun groupRegEx_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B/(.*)", "A/{0}")
+ .rewritesType("A/B/C/D")
+ .into("A/C/D")
+ }
+
+ @Test fun groupRegEx__innerClass_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B/(.*)", "A/{0}")
+ .rewritesType("A/B/C\$D")
+ .into("A/C\$D")
+ }
+
+ @Test fun fieldRule_noRegEx_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B", "A/C")
+ .withFieldSelector("MyField")
+ .rewritesField("A/B", "MyField")
+ .into("A/C", "MyField")
+ }
+
+ @Test fun fieldRule_innerClass_groupRegEx_shouldRewrite() {
+ RuleTester
+ .testThatRule("A/B$(.*)", "A/C\${0}")
+ .rewritesType("A/B\$D")
+ .into("A/C\$D")
+ }
+
+ @Test fun noFieldRule_shouldRewriteEvenWithField() {
+ RuleTester
+ .testThatRule("A/B", "A/C")
+ .rewritesField("A/B", "test")
+ .into("A/C", "test")
+ }
+
+
+ object RuleTester {
+
+ fun testThatRule(from: String, to: String) = RuleTesterStep1(from, to)
+
+ class RuleTesterStep1(val from: String, val to: String) {
+
+ val fieldSelectors: MutableList<String> = mutableListOf()
+
+ fun withFieldSelector(input: String) : RuleTesterStep1 {
+ fieldSelectors.add(input)
+ return this
+ }
+
+ fun rewritesField(inputType: String, inputField: String)
+ = RuleTesterFinalFieldStep(from, to, inputType, inputField, fieldSelectors)
+
+ fun rewritesType(inputType: String)
+ = RuleTesterFinalTypeStep(from, to, inputType, fieldSelectors)
+ }
+
+ class RuleTesterFinalFieldStep(val fromType: String,
+ val toType: String,
+ val inputType: String,
+ val inputField: String,
+ val fieldSelectors: List<String>) {
+
+ fun into(expectedTypeName: String, expectedFieldName: String) {
+ val fieldRule = RewriteRule(fromType, toType, fieldSelectors)
+ val result = fieldRule.apply(JavaField(inputType, inputField))
+ Truth.assertThat(result).isNotNull()
+
+ Truth.assertThat(result!!.owner.fullName).isEqualTo(expectedTypeName)
+ Truth.assertThat(result.name).isEqualTo(expectedFieldName)
+ }
+
+ }
+
+ class RuleTesterFinalTypeStep(val fromType: String,
+ val toType: String,
+ val inputType: String,
+ val fieldSelectors: List<String>) {
+
+ fun into(expectedResult: String) {
+ val fieldRule = RewriteRule(fromType, toType, fieldSelectors)
+ val result = fieldRule.apply(JavaType(inputType))
+ Truth.assertThat(result).isNotNull()
+
+ Truth.assertThat(result).isNotNull()
+ Truth.assertThat(result!!.fullName).isEqualTo(expectedResult)
+ }
+
+ }
+ }
+
+}
+
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocumentTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocumentTest.kt
new file mode 100644
index 0000000..d55687f
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocumentTest.kt
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import com.google.common.truth.Truth
+import org.junit.Test
+import java.nio.charset.StandardCharsets
+import java.nio.file.Paths
+
+class PomDocumentTest {
+
+ @Test fun pom_noRules_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " <type>jar</type>\n" +
+ " <scope>test</scope>\n" +
+ " <optional>true</optional>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf()
+ )
+ }
+
+ @Test fun pom_oneRule_shouldApply() {
+ testRewrite(
+ givenXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <systemPath>test/test</systemPath>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ expectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <systemPath>test/test</systemPath>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0")
+ )
+ )
+ )
+ )
+ }
+
+ @Test fun pom_oneRule_shouldSkipTestScopedRule() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " <scope>test</scope>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0")
+ )
+ )
+ )
+ )
+ }
+
+ @Test fun pom_oneRule_notApplicable() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact2",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0")
+ )
+ )
+ )
+ )
+ }
+
+ @Test fun pom_oneRule_appliedForEachType() {
+ testRewrite(
+ givenXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " <type>test</type>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " <type>compile</type>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ expectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " <type>test</type>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " <type>compile</type>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0")
+ )
+ )
+ )
+ )
+ }
+
+ @Test fun pom_multipleTargets_shouldApplyAll() {
+ testRewrite(
+ givenXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ expectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup2</groupId>\n" +
+ " <artifactId>testArtifact2</artifactId>\n" +
+ " <version>2.0</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0"),
+ PomDependency(
+ groupId = "testGroup2", artifactId = "testArtifact2",
+ version = "2.0"))
+ )
+ )
+ )
+ }
+
+ @Test fun pom_multipleRulesAndTargets_shouldApplyAll_distinct() {
+ testRewrite(
+ givenXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact2</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ expectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " </dependency>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup2</groupId>\n" +
+ " <artifactId>testArtifact2</artifactId>\n" +
+ " <version>2.0</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0"),
+ PomDependency(
+ groupId = "testGroup2", artifactId = "testArtifact2",
+ version = "2.0")
+ )
+ ),
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact2",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0"),
+ PomDependency(
+ groupId = "testGroup2", artifactId = "testArtifact2",
+ version = "2.0"))
+ )
+ )
+ )
+ }
+
+ @Test fun pom_oneRule_hasToKeepExtraAttributesAndRewrite() {
+ testRewrite(
+ givenXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>supportArtifact</artifactId>\n" +
+ " <version>4.0</version>\n" +
+ " <classifier>hey</classifier>\n" +
+ " <type>jar</type>\n" +
+ " <scope>runtime</scope>\n" +
+ " <systemPath>somePath</systemPath>\n" +
+ " <optional>true</optional>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ expectedXml =
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>testGroup</groupId>\n" +
+ " <artifactId>testArtifact</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " <classifier>hey</classifier>\n" +
+ " <type>jar</type>\n" +
+ " <scope>runtime</scope>\n" +
+ " <systemPath>somePath</systemPath>\n" +
+ " <optional>true</optional>\n" +
+ " </dependency>\n" +
+ " </dependencies>",
+ rules = listOf(
+ PomRewriteRule(
+ PomDependency(
+ groupId = "supportGroup", artifactId = "supportArtifact",
+ version = "4.0"),
+ listOf(
+ PomDependency(
+ groupId = "testGroup", artifactId = "testArtifact",
+ version = "1.0")
+ )
+ )
+ )
+ )
+ }
+
+ @Test fun pom_usingEmptyProperties_shouldNotCrash() {
+ val document = loadDocument(
+ " <properties/>\n" +
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>\${groupId.version.property}</artifactId>\n" +
+ " <version>\${groupId.version.property}</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>"
+ )
+
+ Truth.assertThat(document.dependencies).hasSize(1)
+ }
+
+ @Test fun pom_usingProperties_shouldResolve() {
+ val document = loadDocument(
+ " <properties>\n" +
+ " <groupId.version.property>1.0.0</groupId.version.property>\n" +
+ " <groupId.artifactId.property>supportArtifact</groupId.artifactId.property>\n" +
+ " </properties>\n" +
+ " <dependencies>\n" +
+ " <dependency>\n" +
+ " <groupId>supportGroup</groupId>\n" +
+ " <artifactId>\${groupId.artifactId.property}</artifactId>\n" +
+ " <version>\${groupId.version.property}</version>\n" +
+ " </dependency>\n" +
+ " </dependencies>"
+ )
+
+ Truth.assertThat(document.dependencies).hasSize(1)
+
+ val dependency = document.dependencies.first()
+ Truth.assertThat(dependency.version).isEqualTo("1.0.0")
+ Truth.assertThat(dependency.artifactId).isEqualTo("supportArtifact")
+ }
+
+
+ private fun testRewriteToTheSame(givenAndExpectedXml: String, rules: List<PomRewriteRule>) {
+ testRewrite(givenAndExpectedXml, givenAndExpectedXml, rules)
+ }
+
+ private fun testRewrite(givenXml: String, expectedXml : String, rules: List<PomRewriteRule>) {
+ val given =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" " +
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
+ "xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" +
+ " <!-- Some comment -->\n" +
+ " <groupId>test.group</groupId>\n" +
+ " <artifactId>test.artifact.id</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " $givenXml\n" +
+ "</project>\n"
+
+ var expected =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" " +
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
+ "xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" +
+ " <!-- Some comment -->\n" +
+ " <groupId>test.group</groupId>\n" +
+ " <artifactId>test.artifact.id</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " $expectedXml\n" +
+ "</project>\n"
+
+ val file = ArchiveFile(Paths.get("pom.xml"), given.toByteArray())
+ val pomDocument = PomDocument.loadFrom(file)
+ pomDocument.applyRules(rules)
+ pomDocument.saveBackToFileIfNeeded()
+ var strResult = file.data.toString(StandardCharsets.UTF_8)
+
+ // Remove spaces in front of '<' and the back of '>'
+ expected = expected.replace(">[ ]+".toRegex(), ">")
+ expected = expected.replace("[ ]+<".toRegex(), "<")
+
+ strResult = strResult.replace(">[ ]+".toRegex(), ">")
+ strResult = strResult.replace("[ ]+<".toRegex(), "<")
+
+ // Replace newline characters to match the ones we are using in the expected string
+ strResult = strResult.replace("\\r\\n".toRegex(), "\n")
+
+ Truth.assertThat(strResult).isEqualTo(expected)
+ }
+
+ private fun loadDocument(givenXml : String) : PomDocument {
+ val given =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" " +
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
+ "xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" +
+ " <!-- Some comment -->\n" +
+ " <groupId>test.group</groupId>\n" +
+ " <artifactId>test.artifact.id</artifactId>\n" +
+ " <version>1.0</version>\n" +
+ " $givenXml\n" +
+ "</project>\n"
+
+ val file = ArchiveFile(Paths.get("pom.xml"), given.toByteArray())
+ val pomDocument = PomDocument.loadFrom(file)
+ return pomDocument
+ }
+}
+
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRuleTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRuleTest.kt
new file mode 100644
index 0000000..34ebd04
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteRuleTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.pom
+
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class PomRewriteRuleTest {
+
+ @Test fun versions_nullInRule_match() {
+ testVersionsMatch(
+ ruleVersion = null,
+ pomVersion = "27.0.0"
+ )
+ }
+
+ @Test fun versions_nullInPom_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = null
+ )
+ }
+
+ @Test fun versions_nullBoth_match() {
+ testVersionsMatch(
+ ruleVersion = null,
+ pomVersion = null
+ )
+ }
+
+ @Test fun versions_same_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "27.0.0"
+ )
+ }
+
+ @Test fun versions_same_strict_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "[27.0.0]"
+ )
+ }
+
+ @Test fun versions_different_noMatch() {
+ testVersionsDoNotMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "26.0.0"
+ )
+ }
+
+ @Test fun versions_release_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "release"
+ )
+ }
+
+ @Test fun versions_latest_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "latest"
+ )
+ }
+
+ @Test fun versions_range_rightOpen_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "(26.0.0,]"
+ )
+ }
+
+ @Test fun versions_range_rightOpen2_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "(26.0.0,)"
+ )
+ }
+
+ @Test fun versions_range_inclusive_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "[21.0.0,27.0.0]"
+ )
+ }
+
+ @Test fun versions_range_inclusive_noMatch() {
+ testVersionsDoNotMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "[21.0.0,26.0.0]"
+ )
+ }
+
+ @Test fun versions_range_exclusive_noMatch() {
+ testVersionsDoNotMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "[21.0.0,27.0.0)"
+ )
+ }
+
+ @Test fun versions_exclusionRange_match() {
+ testVersionsMatch(
+ ruleVersion = "27.0.0",
+ pomVersion = "(,26.0.0),(26.0.0,)"
+ )
+ }
+
+ private fun testVersionsMatch(ruleVersion: String?, pomVersion: String?) {
+ val from = PomDependency(version = ruleVersion)
+ val pom = PomDependency(version = pomVersion)
+
+ val rule = PomRewriteRule(from, listOf(from))
+
+ Truth.assertThat(rule.validateVersion(pom)).isTrue()
+ }
+
+ private fun testVersionsDoNotMatch(ruleVersion: String?, pomVersion: String?) {
+ val from = PomDependency(version = ruleVersion)
+ val pom = PomDependency(version = pomVersion)
+
+ val rule = PomRewriteRule(from, listOf(from))
+
+ Truth.assertThat(rule.validateVersion(pom)).isFalse()
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassFilterTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassFilterTest.kt
new file mode 100644
index 0000000..2c7d7e2
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassFilterTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassFilterTest {
+
+ @Test fun proGuard_classFilter() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-adaptclassstrings support.Activity, support.Fragment, keep.Me"
+ )
+ .rewritesTo(
+ "-adaptclassstrings test.Activity, test.Fragment, keep.Me"
+ )
+ }
+
+ @Test fun proGuard_classFilter_newLineIgnored() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-adaptclassstrings support.Activity, support.Fragment, keep.Me \n" +
+ " support.Activity"
+ )
+ .rewritesTo(
+ "-adaptclassstrings test.Activity, test.Fragment, keep.Me \n" +
+ " support.Activity"
+ )
+ }
+
+ @Test fun proGuard_classFilter_spacesRespected() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ " -adaptclassstrings support.Activity , support.Fragment,keep.Me "
+ )
+ .rewritesTo(
+ " -adaptclassstrings test.Activity, test.Fragment, keep.Me"
+ )
+ }
+
+ @Test fun proGuard_classFilter_negation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ " -adaptclassstrings !support.Activity, !support.Fragment, !keep.Me "
+ )
+ .rewritesTo(
+ " -adaptclassstrings !test.Activity, !test.Fragment, !keep.Me"
+ )
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest.kt
new file mode 100644
index 0000000..e64590f
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest {
+
+ @Test fun proGuard_classSpec_simple() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep class support.Activity"
+ )
+ .rewritesTo(
+ "-keep class test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_allExistingRules() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep class support.Activity \n" +
+ "-keepclassmembers class support.Activity \n" +
+ "-keepclasseswithmembers class support.Activity \n" +
+ "-keepnames class support.Activity \n" +
+ "-keepclassmembernames class support.Activity \n" +
+ "-keepclasseswithmembernames class support.Activity \n" +
+ "-whyareyoukeeping class support.Activity \n" +
+ "-assumenosideeffects class support.Activity"
+ )
+ .rewritesTo(
+ "-keep class test.Activity \n" +
+ "-keepclassmembers class test.Activity \n" +
+ "-keepclasseswithmembers class test.Activity \n" +
+ "-keepnames class test.Activity \n" +
+ "-keepclassmembernames class test.Activity \n" +
+ "-keepclasseswithmembernames class test.Activity \n" +
+ "-whyareyoukeeping class test.Activity \n" +
+ "-assumenosideeffects class test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_rulesModifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep includedescriptorclasses class support.Activity \n" +
+ "-keep allowshrinking class support.Activity \n" +
+ "-keep allowoptimization class support.Activity \n" +
+ "-keep allowobfuscation class support.Activity \n" +
+ "-keep allowshrinking allowoptimization allowobfuscation class support.Activity \n" +
+ "-keep allowshrinking allowoptimization allowobfuscation class support.Activity"
+ )
+ .rewritesTo(
+ "-keep includedescriptorclasses class test.Activity \n" +
+ "-keep allowshrinking class test.Activity \n" +
+ "-keep allowoptimization class test.Activity \n" +
+ "-keep allowobfuscation class test.Activity \n" +
+ "-keep allowshrinking allowoptimization allowobfuscation class test.Activity \n" +
+ "-keep allowshrinking allowoptimization allowobfuscation class test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_extends() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep class * extends support.Activity \n" +
+ "-keep class support.Fragment extends support.Activity"
+ )
+ .rewritesTo(
+ "-keep class * extends test.Activity \n" +
+ "-keep class test.Fragment extends test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_modifiers_extends() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity"
+ )
+ .testThatGivenProGuard(
+ "-keep !public enum * extends support.Activity \n" +
+ "-keep public !final enum * extends support.Activity"
+ )
+ .rewritesTo(
+ "-keep !public enum * extends test.Activity \n" +
+ "-keep public !final enum * extends test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep @support.Annotation public class support.Activity \n" +
+ "-keep @some.Annotation public class support.Activity"
+ )
+ .rewritesTo(
+ "-keep @test.Annotation public class test.Activity \n" +
+ "-keep @some.Annotation public class test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_annotation_extends() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep @support.Annotation public class * extends support.Activity\n" +
+ "-keep @some.Annotation !public class * extends support.Activity"
+ )
+ .rewritesTo(
+ "-keep @test.Annotation public class * extends test.Activity\n" +
+ "-keep @some.Annotation !public class * extends test.Activity"
+ )
+ }
+
+ @Test fun proGuard_classSpec_annotation_extends_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep \t @support.Annotation \t public class * extends support.Activity"
+ )
+ .rewritesTo(
+ "-keep \t @test.Annotation \t public class * extends test.Activity"
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldTypeSelector.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldTypeSelector.kt
new file mode 100644
index 0000000..2832385
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldTypeSelector.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest_FieldTypeSelector {
+
+ @Test fun proGuard_fieldTypeSelector() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " support.Activity height; \n" +
+ " support.Fragment *; \n" +
+ " keep.Me width; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " test.Activity height; \n" +
+ " test.Fragment *; \n" +
+ " keep.Me width; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldTypeSelector_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public support.Fragment height; \n" +
+ " !public !static support.Fragment height; \n" +
+ " !protected support.Fragment height; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public test.Fragment height; \n" +
+ " !public !static test.Fragment height; \n" +
+ " !protected test.Fragment height; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldTypeSelector_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation support.Fragment height; \n" +
+ " @some.Annotation support.Fragment height; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation test.Fragment height; \n" +
+ " @some.Annotation test.Fragment height; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldTypeSelector_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public support.Fragment height; \n" +
+ " @support.Annotation !public !static support.Fragment height; \n" +
+ " @support.Annotation !protected volatile support.Fragment height; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public test.Fragment height; \n" +
+ " @test.Annotation !public !static test.Fragment height; \n" +
+ " @test.Annotation !protected volatile test.Fragment height; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldTypeSelector_modifiers_annotation_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public static \t support.Fragment height ; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public static \t test.Fragment height ; \n" +
+ "}"
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldsSelector.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldsSelector.kt
new file mode 100644
index 0000000..6f6a1f9
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_FieldsSelector.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest_FieldsSelector {
+
+ @Test fun proGuard_fieldsSelector_minimal() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * extends support.Activity { \n" +
+ " <fields>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * extends test.Activity { \n" +
+ " <fields>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldsSelector_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ )
+ .forGivenTypesMap(
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public <fields>; \n" +
+ " public static <fields>; \n" +
+ " !private !protected <fields>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public <fields>; \n" +
+ " public static <fields>; \n" +
+ " !private !protected <fields>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldsSelector_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public <fields>; \n" +
+ " @support.Annotation public static <fields>; \n" +
+ " @support.Annotation !private !protected <fields>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public <fields>; \n" +
+ " @test.Annotation public static <fields>; \n" +
+ " @test.Annotation !private !protected <fields>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_fieldsSelector_modifiers_annotation_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public \t <fields> ; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public \t <fields> ; \n" +
+ "}"
+ )
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodInitSelector.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodInitSelector.kt
new file mode 100644
index 0000000..9a792cf
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodInitSelector.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest_MethodInitSelector {
+
+ @Test fun proGuard_methodsInitSelector() {
+ ProGuardTester
+ .forGivenPrefixes(
+ )
+ .forGivenTypesMap(
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " <methods>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " <methods>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodsInitSelector_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ )
+ .forGivenTypesMap(
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public <methods>; \n" +
+ " public static <methods>; \n" +
+ " public !static <methods>; \n" +
+ " !private static <methods>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public <methods>; \n" +
+ " public static <methods>; \n" +
+ " public !static <methods>; \n" +
+ " !private static <methods>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodsInitSelector_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public <methods>; \n" +
+ " @support.Annotation public static <methods>; \n" +
+ " @support.Annotation public !static <methods>; \n" +
+ " @support.Annotation !private static <methods>; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public <methods>; \n" +
+ " @test.Annotation public static <methods>; \n" +
+ " @test.Annotation public !static <methods>; \n" +
+ " @test.Annotation !private static <methods>; \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodInitSelector() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " <init>(); \n" +
+ " <init>(*); \n" +
+ " <init>(...); \n" +
+ " <init>(support.Activity); \n" +
+ " <init>(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " <init>(); \n" +
+ " <init>(*); \n" +
+ " <init>(...); \n" +
+ " <init>(test.Activity); \n" +
+ " <init>(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodInitSelector_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public <init>(); \n" +
+ " public static <init>(*); \n" +
+ " !public !static <init>(...); \n" +
+ " !private static <init>(support.Activity); \n" +
+ " public !abstract <init>(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public <init>(); \n" +
+ " public static <init>(*); \n" +
+ " !public !static <init>(...); \n" +
+ " !private static <init>(test.Activity); \n" +
+ " public !abstract <init>(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodInitSelector_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation <init>(); \n" +
+ " @support.Annotation <init>(*); \n" +
+ " @support.Annotation <init>(...); \n" +
+ " @keep.Me <init>(support.Activity); \n" +
+ " @support.Annotation <init>(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation <init>(); \n" +
+ " @test.Annotation <init>(*); \n" +
+ " @test.Annotation <init>(...); \n" +
+ " @keep.Me <init>(test.Activity); \n" +
+ " @test.Annotation <init>(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodInitSelector_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public <init>(); \n" +
+ " @support.Annotation public static <init>(*); \n" +
+ " @support.Annotation !public !static <init>(...); \n" +
+ " @support.Annotation !private static <init>(support.Activity); \n" +
+ " @support.Annotation public !abstract <init>(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public <init>(); \n" +
+ " @test.Annotation public static <init>(*); \n" +
+ " @test.Annotation !public !static <init>(...); \n" +
+ " @test.Annotation !private static <init>(test.Activity); \n" +
+ " @test.Annotation public !abstract <init>(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodInitSelector_modifiers_annotation_test() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public !abstract \t <init> ( support.Activity , support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public !abstract \t <init> (test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodSelectorWithReturnType.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodSelectorWithReturnType.kt
new file mode 100644
index 0000000..d9960b4
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_MethodSelectorWithReturnType.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest_MethodSelectorWithReturnType {
+
+ @Test fun proGuard_methodReturnTypeSelector() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " void get*(); \n" +
+ " void get*(...); \n" +
+ " void get*(*); \n" +
+ " void get*(support.Activity); \n" +
+ " void get?(support.Activity); \n" +
+ " void get(support.Activity); \n" +
+ " void *(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " void get*(); \n" +
+ " void get*(...); \n" +
+ " void get*(*); \n" +
+ " void get*(test.Activity); \n" +
+ " void get?(test.Activity); \n" +
+ " void get(test.Activity); \n" +
+ " void *(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_voidResult() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " void get(); \n" +
+ " void get(...); \n" +
+ " void get(*); \n" +
+ " void get(support.Activity); \n" +
+ " void get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " void get(); \n" +
+ " void get(...); \n" +
+ " void get(*); \n" +
+ " void get(test.Activity); \n" +
+ " void get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_starResult() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " * get(); \n" +
+ " * get(...); \n" +
+ " * get(*); \n" +
+ " * get(support.Activity); \n" +
+ " * get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " * get(); \n" +
+ " * get(...); \n" +
+ " * get(*); \n" +
+ " * get(test.Activity); \n" +
+ " * get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " support.Fragment get(); \n" +
+ " support.Fragment get(...); \n" +
+ " support.Fragment get(*); \n" +
+ " support.Fragment get(support.Activity); \n" +
+ " support.Fragment get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " test.Fragment get(); \n" +
+ " test.Fragment get(...); \n" +
+ " test.Fragment get(*); \n" +
+ " test.Fragment get(test.Activity); \n" +
+ " test.Fragment get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult_wildcards() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " support.Fragment get*(); \n" +
+ " support.Fragment get?(...); \n" +
+ " support.Fragment *(*); \n" +
+ " support.Fragment *(support.Activity); \n" +
+ " support.Fragment *(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " test.Fragment get*(); \n" +
+ " test.Fragment get?(...); \n" +
+ " test.Fragment *(*); \n" +
+ " test.Fragment *(test.Activity); \n" +
+ " test.Fragment *(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public support.Fragment get(); \n" +
+ " public static support.Fragment get(...); \n" +
+ " !public !static support.Fragment get(*); \n" +
+ " private support.Fragment get(support.Activity); \n" +
+ " public abstract support.Fragment get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public test.Fragment get(); \n" +
+ " public static test.Fragment get(...); \n" +
+ " !public !static test.Fragment get(*); \n" +
+ " private test.Fragment get(test.Activity); \n" +
+ " public abstract test.Fragment get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation support.Fragment get(); \n" +
+ " @support.Annotation support.Fragment get(...); \n" +
+ " @support.Annotation support.Fragment get(*); \n" +
+ " @keep.Me support.Fragment get(support.Activity); \n" +
+ " @support.Annotation support.Fragment get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation test.Fragment get(); \n" +
+ " @test.Annotation test.Fragment get(...); \n" +
+ " @test.Annotation test.Fragment get(*); \n" +
+ " @keep.Me test.Fragment get(test.Activity); \n" +
+ " @test.Annotation test.Fragment get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public support.Fragment get(); \n" +
+ " @support.Annotation public static support.Fragment get(...); \n" +
+ " @support.Annotation !public !static support.Fragment get(*); \n" +
+ " @support.Annotation private support.Fragment get(support.Activity); \n" +
+ " @support.Annotation public abstract support.Fragment get(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public test.Fragment get(); \n" +
+ " @test.Annotation public static test.Fragment get(...); \n" +
+ " @test.Annotation !public !static test.Fragment get(*); \n" +
+ " @test.Annotation private test.Fragment get(test.Activity); \n" +
+ " @test.Annotation public abstract test.Fragment get(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_methodReturnTypeSelector_typeResult_modifiers_annotation_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation support.Fragment \t get(support.Activity , support.Fragment , keep.Please) ; \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation test.Fragment \t get(test.Activity, test.Fragment, keep.Please) ; \n" +
+ "}"
+ )
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_NamedCtorSelector.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_NamedCtorSelector.kt
new file mode 100644
index 0000000..21b8b8c
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ClassSpecTest_NamedCtorSelector.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ClassSpecTest_NamedCtorSelector {
+
+ @Test fun proGuard_ctorSelector() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " support.Activity(); \n" +
+ " support.Activity(...); \n" +
+ " support.Activity(*); \n" +
+ " support.Activity(support.Activity); \n" +
+ " support.Activity(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " test.Activity(); \n" +
+ " test.Activity(...); \n" +
+ " test.Activity(*); \n" +
+ " test.Activity(test.Activity); \n" +
+ " test.Activity(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_ctorSelector_modifiers() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " public support.Activity(); \n" +
+ " public static support.Activity(...); \n" +
+ " !private support.Activity(*); \n" +
+ " !public !static support.Activity(support.Activity); \n" +
+ " !protected support.Activity(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " public test.Activity(); \n" +
+ " public static test.Activity(...); \n" +
+ " !private test.Activity(*); \n" +
+ " !public !static test.Activity(test.Activity); \n" +
+ " !protected test.Activity(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_ctorSelector_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation support.Activity(); \n" +
+ " @support.Annotation support.Activity(...); \n" +
+ " @support.Annotation support.Activity(*); \n" +
+ " @support.Annotation support.Activity(support.Activity); \n" +
+ " @support.Annotation support.Activity(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation test.Activity(); \n" +
+ " @test.Annotation test.Activity(...); \n" +
+ " @test.Annotation test.Activity(*); \n" +
+ " @test.Annotation test.Activity(test.Activity); \n" +
+ " @test.Annotation test.Activity(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_ctorSelector_modifiers_annotation() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation public support.Activity(); \n" +
+ " @support.Annotation public static support.Activity(...); \n" +
+ " @support.Annotation !private support.Activity(*); \n" +
+ " @support.Annotation !public !static support.Activity(support.Activity); \n" +
+ " @support.Annotation !protected support.Activity(support.Activity, support.Fragment, keep.Please); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation public test.Activity(); \n" +
+ " @test.Annotation public static test.Activity(...); \n" +
+ " @test.Annotation !private test.Activity(*); \n" +
+ " @test.Annotation !public !static test.Activity(test.Activity); \n" +
+ " @test.Annotation !protected test.Activity(test.Activity, test.Fragment, keep.Please); \n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_ctorSelector_modifiers_annotation_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * { \n" +
+ " @support.Annotation !protected \t support.Activity( support.Activity ); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * { \n" +
+ " @test.Annotation !protected \t test.Activity(test.Activity); \n" +
+ "}"
+ )
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
new file mode 100644
index 0000000..cae21d0
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.TransformationContext
+import com.google.common.truth.Truth
+import java.nio.charset.StandardCharsets
+import java.nio.file.Paths
+
+
+/**
+ * Helper to test ProGuard rewriting logic using lightweight syntax.
+ */
+object ProGuardTester {
+
+ private var javaTypes = emptyList<Pair<String, String>>()
+ private var proGuardTypes = emptyList<Pair<ProGuardType, ProGuardType>>()
+ private var prefixes = emptyList<String>()
+
+ fun forGivenPrefixes(vararg prefixes: String) : ProGuardTester {
+ this.prefixes = prefixes.toList()
+ return this
+ }
+
+ fun forGivenTypesMap(vararg rules: Pair<String, String>) : ProGuardTester {
+ this.javaTypes = rules.toList()
+ return this
+ }
+
+ fun forGivenProGuardMap(vararg rules: Pair<String, String>) : ProGuardTester {
+ this.proGuardTypes = rules.map {
+ ProGuardType.fromDotNotation(it.first) to ProGuardType.fromDotNotation(it.second) }
+ .toList()
+ return this
+ }
+
+ fun testThatGivenType(givenType: String) : ProGuardTesterForType {
+ return ProGuardTesterForType(createConfig(), givenType)
+ }
+
+ fun testThatGivenArguments(givenArgs: String) : ProGuardTesterForArgs {
+ return ProGuardTesterForArgs(createConfig(), givenArgs)
+ }
+
+ fun testThatGivenProGuard(given: String) : ProGuardTesterForFile {
+ return ProGuardTesterForFile(createConfig(), given)
+ }
+
+ private fun createConfig() : Config {
+ return Config(
+ restrictToPackagePrefixes = prefixes,
+ rewriteRules = emptyList(),
+ pomRewriteRules = emptyList(),
+ typesMap = TypesMap(
+ types = javaTypes.map { JavaType(it.first) to JavaType(it.second) }.toMap(),
+ fields = emptyMap()),
+ proGuardMap = ProGuardTypesMap(proGuardTypes.toMap()))
+ }
+
+
+ class ProGuardTesterForFile(private val config: Config, private val given: String) {
+
+ fun rewritesTo(expected: String) {
+ val context = TransformationContext(config)
+ val transformer = ProGuardTransformer(context)
+ val file = ArchiveFile(Paths.get("proguard.txt"), given.toByteArray())
+ transformer.runTransform(file)
+
+ val result = file.data.toString(StandardCharsets.UTF_8)
+
+ Truth.assertThat(result).isEqualTo(expected)
+ }
+
+ }
+
+ class ProGuardTesterForType(private val config: Config, private val given: String) {
+
+ fun getsRewrittenTo(expectedType: String) {
+ val context = TransformationContext(config)
+ val mapper = ProGuardTypesMapper(context)
+ val result = mapper.replaceType(given)
+
+ Truth.assertThat(result).isEqualTo(expectedType)
+ }
+
+ }
+
+ class ProGuardTesterForArgs(private val config: Config, private val given: String) {
+
+ fun getRewrittenTo(expectedArguments: String) {
+ val context = TransformationContext(config)
+ val mapper = ProGuardTypesMapper(context)
+ val result = mapper.replaceMethodArgs(given)
+
+ Truth.assertThat(result).isEqualTo(expectedArguments)
+ }
+ }
+
+}
+
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapperTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapperTest.kt
new file mode 100644
index 0000000..5e12aff
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTypesMapperTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ProGuardTypesMapperTest {
+
+ @Test fun proGuard_typeMapper_wildcard_simple() {
+ ProGuardTester
+ .testThatGivenType("*")
+ .getsRewrittenTo("*")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_double() {
+ ProGuardTester
+ .testThatGivenType("**")
+ .getsRewrittenTo("**")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_composed() {
+ ProGuardTester
+ .testThatGivenType("**/*")
+ .getsRewrittenTo("**/*")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_viaMap() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenProGuardMap(
+ "support/v7/*" to "test/v7/*"
+ )
+ .testThatGivenType("support.v7.*")
+ .getsRewrittenTo("test.v7.*")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_viaMap2() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenProGuardMap(
+ "support/v7/**" to "test/v7/**"
+ )
+ .testThatGivenType("support.v7.**")
+ .getsRewrittenTo("test.v7.**")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_viaTypesMap() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/v7/Activity" to "test/v7/Activity"
+ )
+ .testThatGivenType("support.v7.Activity")
+ .getsRewrittenTo("test.v7.Activity")
+ }
+
+ @Test fun proGuard_typeMapper_wildcard_notFoundInMap() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenProGuardMap(
+ "support/**" to "test/**"
+ )
+ .testThatGivenType("keep.me.**")
+ .getsRewrittenTo("keep.me.**")
+ }
+
+ @Test fun proGuard_typeMapper_differentPrefix_notRewritten() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "hello/Activity" to "test/Activity"
+ )
+ .testThatGivenType("hello.Activity")
+ .getsRewrittenTo("hello.Activity")
+ }
+
+ @Test fun proGuard_typeMapper_differentPrefix_wildcard_getsRewritten() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenProGuardMap(
+ "hello/**" to "test/**"
+ )
+ .testThatGivenType("hello.**")
+ .getsRewrittenTo("test.**")
+ }
+
+ @Test fun proGuard_typeMapper_innerClass() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity\$InnerClass" to "test/Activity\$InnerClass"
+ )
+ .testThatGivenType("support.Activity\$InnerClass")
+ .getsRewrittenTo("test.Activity\$InnerClass")
+ }
+
+ @Test fun proGuard_typeMapper_innerClass_wildcard() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenProGuardMap(
+ "**R\$Attrs" to "**R2\$Attrs"
+ )
+ .testThatGivenType("**R\$Attrs")
+ .getsRewrittenTo("**R2\$Attrs")
+ }
+
+ @Test fun proGuard_argsMapper_tripleDots() {
+ ProGuardTester
+ .testThatGivenArguments("...")
+ .getRewrittenTo("...")
+ }
+
+ @Test fun proGuard_argsMapper_wildcard() {
+ ProGuardTester
+ .testThatGivenArguments("*")
+ .getRewrittenTo("*")
+ }
+
+ @Test fun proGuard_argsMapper_wildcards() {
+ ProGuardTester
+ .testThatGivenArguments("**, **")
+ .getRewrittenTo("**, **")
+ }
+
+ @Test fun proGuard_argsMapper_viaMaps() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity"
+ )
+ .forGivenProGuardMap(
+ "support/v7/**" to "test/v7/**"
+ )
+ .testThatGivenArguments("support.Activity, support.v7.**, keep.Me")
+ .getRewrittenTo("test.Activity, test.v7.**, keep.Me")
+ }
+
+ @Test fun proGuard_argsMapper_viaMaps_spaces() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity"
+ )
+ .forGivenProGuardMap(
+ "support/v7/**" to "test/v7/**"
+ )
+ .testThatGivenArguments(" support.Activity , \t support.v7.**, keep.Me ")
+ .getRewrittenTo("test.Activity, test.v7.**, keep.Me")
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProguardSamplesTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProguardSamplesTest.kt
new file mode 100644
index 0000000..0542e7d
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProguardSamplesTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.proguard
+
+import org.junit.Test
+
+class ProguardSamplesTest {
+
+ @Test fun proGuard_sample() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "android/app/",
+ "android/view/",
+ "android/content/",
+ "android/os/",
+ "android/webkit/"
+ )
+ .forGivenTypesMap(
+ "android/app/Activity" to "test/app/Activity",
+ "android/app/Application" to "test/app/Application",
+ "android/view/View" to "test/view/View",
+ "android/view/MenuItem" to "test/view/MenuItem",
+ "android/content/Context" to "test/content/Context",
+ "android/os/Parcelable" to "test/os/Parcelable",
+ "android/webkit/JavascriptInterface" to "test/webkit/JavascriptInterface"
+ )
+ .testThatGivenProGuard(
+ "-injars bin/classes \n" +
+ "-injars libs \n" +
+ "-outjars bin/classes-processed.jar \n" +
+ "-libraryjars /usr/local/java/android-sdk/platforms/android-9/android.jar \n" +
+ "\n" +
+ "-dontpreverify \n" +
+ "-repackageclasses '' \n" +
+ "-allowaccessmodification \n" +
+ "-optimizations !code/simplification/arithmetic \n" +
+ "-keepattributes *Annotation* \n" +
+ "\n" +
+ "-keep public class * extends android.app.Activity \n" +
+ "-keep public class * extends android.app.Application \n" +
+ " \n" +
+ "-keep public class * extends android.view.View { \n" +
+ " public <init>(android.content.Context); \n" +
+ " public <init>(android.content.Context, android.util.AttributeSet); \n" +
+ " public <init>(android.content.Context, android.util.AttributeSet, int); \n" +
+ " public void set*(...); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclasseswithmembers class * { \n" +
+ " public <init>(android.content.Context, android.util.AttributeSet); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclasseswithmembers class * { \n" +
+ " public <init>(android.content.Context, android.util.AttributeSet, int); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * extends android.content.Context { \n" +
+ " public void *(android.view.View); \n" +
+ " public void *(android.view.MenuItem); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * implements android.os.Parcelable { \n" +
+ " static ** CREATOR; \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class **.R\$* { \n" +
+ " public static <fields>; \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * { \n" +
+ " @android.webkit.JavascriptInterface <methods>; \n" +
+ "} "
+ )
+ .rewritesTo(
+ "-injars bin/classes \n" +
+ "-injars libs \n" +
+ "-outjars bin/classes-processed.jar \n" +
+ "-libraryjars /usr/local/java/android-sdk/platforms/android-9/android.jar \n" +
+ "\n" +
+ "-dontpreverify \n" +
+ "-repackageclasses '' \n" +
+ "-allowaccessmodification \n" +
+ "-optimizations !code/simplification/arithmetic \n" +
+ "-keepattributes *Annotation* \n" +
+ "\n" +
+ "-keep public class * extends test.app.Activity \n" +
+ "-keep public class * extends test.app.Application \n" +
+ " \n" +
+ "-keep public class * extends test.view.View { \n" +
+ " public <init>(test.content.Context); \n" +
+ " public <init>(test.content.Context, android.util.AttributeSet); \n" +
+ " public <init>(test.content.Context, android.util.AttributeSet, int); \n" +
+ " public void set*(...); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclasseswithmembers class * { \n" +
+ " public <init>(test.content.Context, android.util.AttributeSet); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclasseswithmembers class * { \n" +
+ " public <init>(test.content.Context, android.util.AttributeSet, int); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * extends test.content.Context { \n" +
+ " public void *(test.view.View); \n" +
+ " public void *(test.view.MenuItem); \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * implements test.os.Parcelable { \n" +
+ " static ** CREATOR; \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class **.R\$* { \n" +
+ " public static <fields>; \n" +
+ "} \n" +
+ "\n" +
+ "-keepclassmembers class * { \n" +
+ " @test.webkit.JavascriptInterface <methods>; \n" +
+ "} "
+ )
+ }
+
+ @Test fun proGuard_sample2() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "android/support/v7/"
+ )
+ .forGivenTypesMap(
+ "android/support/v7/preference/Preference" to "test/Preference"
+ )
+ .testThatGivenProGuard(
+ "-keep public class android.support.v7.preference.Preference {\n" +
+ " public <init>(android.content.Context, android.util.AttributeSet);\n" +
+ "}\n" +
+ "-keep public class * extends android.support.v7.preference.Preference {\n" +
+ " public <init>(android.content.Context, android.util.AttributeSet);\n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class test.Preference {\n" +
+ " public <init>(android.content.Context, android.util.AttributeSet);\n" +
+ "}\n" +
+ "-keep public class * extends test.Preference {\n" +
+ " public <init>(android.content.Context, android.util.AttributeSet);\n" +
+ "}"
+ )
+ }
+
+ @Test fun proGuard_sample3() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "android/support/design/",
+ "android/support/v7/"
+ )
+ .forGivenTypesMap(
+ "support/Fragment" to "test/Fragment",
+ "android/support/v7/widget/RoundRectDrawable" to "test/RoundRectDrawable"
+ )
+ .forGivenProGuardMap(
+ "android/support/design.**" to "test/design.**",
+ "android/support/design/R\$*" to "test/design/R\$*"
+ )
+ .testThatGivenProGuard(
+ "-dontwarn android.support.design.**\n" +
+ "-keep class android.support.design.** { *; }\n" +
+ "-keep interface android.support.design.** { *; }\n" +
+ "-keep public class android.support.design.R\$* { *; }\n" +
+ "-keep class android.support.v7.widget.RoundRectDrawable { *; }"
+ )
+ .rewritesTo(
+ "-dontwarn test.design.**\n" +
+ "-keep class test.design.** { *; }\n" +
+ "-keep interface test.design.** { *; }\n" +
+ "-keep public class test.design.R\$* { *; }\n" +
+ "-keep class test.RoundRectDrawable { *; }"
+ )
+ }
+
+ @Test fun proGuard_sample4() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "android/support/design/",
+ "android/support/v7/",
+ "android/support/v4/"
+ )
+ .forGivenTypesMap(
+ "android/support/v7/widget/LinearLayoutManager" to "test/LinearLayoutManager",
+ "android/support/v4/view/ActionProvider" to "test/ActionProvider"
+ )
+ .forGivenProGuardMap(
+ "android/support/v7/**" to "test/v7/**",
+ "android/support/v7/widget/**" to "test/v7/widget/**",
+ "android/support/v7/internal/widget/**" to "test/v7/internal/widget/**",
+ "android/support/v7/internal/**" to "test/v7/internal/**"
+ )
+ .testThatGivenProGuard(
+ "-dontwarn android.support.v7.**\n" +
+ "-keep public class android.support.v7.widget.** { *; }\n" +
+ "-keep public class android.support.v7.internal.widget.** { *; }\n" +
+ "-keep class android.support.v7.widget.LinearLayoutManager { *; }\n" +
+ "-keep class android.support.v7.internal.** { *; }\n" +
+ "-keep interface android.support.v7.internal.** { *; }\n" +
+ "\n" +
+ "-keep class android.support.v7.** { *; }\n" +
+ "-keep interface android.support.v7.** { *; }\n" +
+ "\n" +
+ "-keep public class * extends android.support.v4.view.ActionProvider {\n" +
+ " public <init>(android.content.Context);"
+ )
+ .rewritesTo(
+ "-dontwarn test.v7.**\n" +
+ "-keep public class test.v7.widget.** { *; }\n" +
+ "-keep public class test.v7.internal.widget.** { *; }\n" +
+ "-keep class test.LinearLayoutManager { *; }\n" +
+ "-keep class test.v7.internal.** { *; }\n" +
+ "-keep interface test.v7.internal.** { *; }\n" +
+ "\n" +
+ "-keep class test.v7.** { *; }\n" +
+ "-keep interface test.v7.** { *; }\n" +
+ "\n" +
+ "-keep public class * extends test.ActionProvider {\n" +
+ " public <init>(android.content.Context);"
+ )
+ }
+
+ @Test fun proGuard_sample5() {
+ ProGuardTester
+ .forGivenPrefixes(
+ "support/"
+ )
+ .forGivenTypesMap(
+ "support/Activity" to "test/Activity",
+ "support/Fragment" to "test/Fragment",
+ "support/Annotation" to "test/Annotation"
+ )
+ .testThatGivenProGuard(
+ "-keep public class * extends support.Activity { \n" +
+ " public static <fields>; \n" +
+ " public !static <methods>; \n" +
+ " public support.Fragment height; \n" +
+ " public static <fields>; \n" +
+ " public not.related.Type width; public support.Fragment width; \n" +
+ " ignoreMe; \n" +
+ " @support.Annotation public support.Fragment get(); \n" +
+ "}"
+ )
+ .rewritesTo(
+ "-keep public class * extends test.Activity { \n" +
+ " public static <fields>; \n" +
+ " public !static <methods>; \n" +
+ " public test.Fragment height; \n" +
+ " public static <fields>; \n" +
+ " public not.related.Type width; public test.Fragment width; \n" +
+ " ignoreMe; \n" +
+ " @test.Annotation public test.Fragment get(); \n" +
+ "}"
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
new file mode 100644
index 0000000..5788b40
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.core.transform.resource
+
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.common.truth.Truth
+import org.junit.Test
+import java.nio.charset.Charset
+
+class XmlResourcesTransformerTest {
+
+ @Test fun layout_noPrefix_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ prefixes = listOf(),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_noRule_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ prefixes = listOf("android/support/v7/"),
+ map = mapOf()
+ )
+ }
+
+ @Test fun layout_notApplicablePrefix_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ prefixes = listOf("android/support/v14/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_notApplicablePrefix2_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<my.android.support.v7.preference.Preference>\n" +
+ "</my.android.support.v7.preference.Preference>",
+ prefixes = listOf("android/support/v7/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_notApplicableRule_noChange() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support2/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_oneRewrite() {
+ testRewrite(
+ givenXml =
+ "<android.support.v7.preference.Preference/>",
+ expectedXml =
+ "<android.test.pref.Preference/>",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_attribute_oneRewrite() {
+ testRewrite(
+ givenXml =
+ "<android.support.v7.preference.Preference \n" +
+ " someAttribute=\"android.support.v7.preference.Preference\"/>",
+ expectedXml =
+ "<android.test.pref.Preference \n" +
+ " someAttribute=\"android.support.v7.preference.Preference\"/>",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_twoRewrites() {
+ testRewrite(
+ givenXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ expectedXml =
+ "<android.test.pref.Preference>\n" +
+ "</android.test.pref.Preference>",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_viewTag_simple() {
+ testRewrite(
+ givenXml =
+ "<view class=\"android.support.v7.preference.Preference\">",
+ expectedXml =
+ "<view class=\"android.test.pref.Preference\">",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_viewTag_stuffAround() {
+ testRewrite(
+ givenXml =
+ "<view notRelated=\"true\" " +
+ " class=\"android.support.v7.preference.Preference\"" +
+ " ignoreMe=\"android.support.v7.preference.Preference\">",
+ expectedXml =
+ "<view notRelated=\"true\" " +
+ " class=\"android.test.pref.Preference\"" +
+ " ignoreMe=\"android.support.v7.preference.Preference\">",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_viewInText_notMatched() {
+ testRewriteToTheSame(
+ givenAndExpectedXml =
+ "<test attribute=\"view\" class=\"android.support.v7.preference.Preference\">",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_onePrefix_oneRule_identity() {
+ testRewrite(
+ givenXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ expectedXml =
+ "<android.support.v7.preference.Preference>\n" +
+ "</android.support.v7.preference.Preference>",
+ prefixes = listOf("android/support/"),
+ map = mapOf(
+ "android/support/v7/preference/Preference" to "android/support/v7/preference/Preference"
+ )
+ )
+ }
+
+ @Test fun layout_twoPrefixes_threeRules_multipleRewrites() {
+ testRewrite(
+ givenXml =
+ "<android.support.v7.preference.Preference>\n" +
+ " <android.support.v14.preference.DialogPreference" +
+ " someAttribute=\"someValue\"/>\n" +
+ " <android.support.v14.preference.DialogPreference" +
+ " someAttribute=\"someValue2\"/>\n" +
+ " <!-- This one should be ignored --> \n" +
+ " <android.support.v21.preference.DialogPreference" +
+ " someAttribute=\"someValue2\"/>\n" +
+ "</android.support.v7.preference.Preference>\n" +
+ "\n" +
+ "<android.support.v7.preference.ListPreference/>",
+ expectedXml =
+ "<android.test.pref.Preference>\n" +
+ " <android.test14.pref.DialogPreference" +
+ " someAttribute=\"someValue\"/>\n" +
+ " <android.test14.pref.DialogPreference" +
+ " someAttribute=\"someValue2\"/>\n" +
+ " <!-- This one should be ignored --> \n" +
+ " <android.support.v21.preference.DialogPreference" +
+ " someAttribute=\"someValue2\"/>\n" +
+ "</android.test.pref.Preference>\n" +
+ "\n" +
+ "<android.test.pref.ListPref/>",
+ prefixes = listOf(
+ "android/support/v7/",
+ "android/support/v14/"
+ ),
+ map = mapOf(
+ "android/support/v7/preference/ListPreference" to "android/test/pref/ListPref",
+ "android/support/v7/preference/Preference" to "android/test/pref/Preference",
+ "android/support/v14/preference/DialogPreference" to "android/test14/pref/DialogPreference",
+ "android/support/v21/preference/DialogPreference" to "android/test21/pref/DialogPreference"
+ )
+ )
+ }
+
+ private fun testRewriteToTheSame(givenAndExpectedXml: String,
+ prefixes: List<String>,
+ map: Map<String, String>) {
+ testRewrite(givenAndExpectedXml, givenAndExpectedXml, prefixes, map)
+ }
+
+ private fun testRewrite(givenXml : String,
+ expectedXml : String,
+ prefixes: List<String>,
+ map: Map<String, String>) {
+ val given =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "$givenXml\n"
+
+ val expected =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "$expectedXml\n"
+
+ val typesMap = TypesMap(map.map{ JavaType(it.key) to JavaType(it.value) }.toMap(),
+ emptyMap())
+ val config = Config(prefixes, emptyList(), emptyList(), typesMap, ProGuardTypesMap.EMPTY)
+ val context = TransformationContext(config)
+ val processor = XmlResourcesTransformer(context)
+ val result = processor.transform(given.toByteArray())
+ val strResult = result.toString(Charset.defaultCharset())
+
+ Truth.assertThat(strResult).isEqualTo(expected)
+ }
+}
+
diff --git a/jetifier/jetifier/gradle-plugin/build.gradle b/jetifier/jetifier/gradle-plugin/build.gradle
new file mode 100644
index 0000000..710c69f
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+version '1.0'
+
+apply plugin: 'maven-publish'
+
+dependencies {
+ compile project(':core')
+ compileOnly gradleApi()
+}
+
+// Task to create a jar with all the required dependencies bundled inside
+task fatJar(type: Jar) {
+ baseName = project.name + '-all'
+ from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } }
+ with jar
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ }
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt
new file mode 100644
index 0000000..e8acb25
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierExtension.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.plugin.gradle
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.file.FileCollection
+
+
+/**
+ * Defines methods that can be used in gradle on the "jetifier" object and triggers [JetifierTask].
+ */
+open class JetifierExtension(val project : Project) {
+
+ companion object {
+ const val TASK_NAME : String = "Jetifier"
+ }
+
+ /**
+ * Handles dependency defined via string notation (like group:artifact:version).
+ */
+ fun process(dependencyNotation: String): FileCollection {
+ return process(project.getDependencies().create(dependencyNotation))
+ }
+
+ /**
+ * Handles dependency.
+ */
+ fun process(dependency: Dependency): FileCollection {
+ val configuration = project.configurations.detachedConfiguration()
+ configuration.dependencies.add(dependency)
+ return process(configuration)
+ }
+
+ /**
+ * Handles collections of files. Defined e.g. via files() or directly from a configuration.
+ */
+ fun process(files: FileCollection): FileCollection {
+ var task = project.tasks.findByName(TASK_NAME) as JetifierTask?
+ if (task == null) {
+ task = project.tasks.create(TASK_NAME, JetifierTask::class.java)
+ }
+ task!!.addFilesToProcess(files)
+ return task.outputs.files
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierLoggerAdapter.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierLoggerAdapter.kt
new file mode 100644
index 0000000..9d85851
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierLoggerAdapter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.plugin.gradle
+
+import android.support.tools.jetifier.core.utils.LogConsumer
+import org.gradle.api.logging.Logger
+
+/**
+ * Logging adapter to hook jetfier logging into gradle.
+ */
+class JetifierLoggerAdapter(val gradleLogger: Logger) : LogConsumer {
+
+ override fun error(message: String) {
+ gradleLogger.error(message)
+ }
+
+ override fun info(message: String) {
+ gradleLogger.info(message)
+ }
+
+ override fun verbose(message: String) {
+ gradleLogger.info(message)
+ }
+
+ override fun debug(message: String) {
+ gradleLogger.debug(message)
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierPlugin.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierPlugin.kt
new file mode 100644
index 0000000..b63bc9d
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierPlugin.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.plugin.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * This servers as the main entry point of this plugin and registers the extension object.
+ */
+open class JetifierPlugin : Plugin<Project> {
+
+ companion object {
+ const val GROOVY_OBJECT_NAME : String = "jetifier"
+ }
+
+ override fun apply(project: Project) {
+ project.getExtensions().create(GROOVY_OBJECT_NAME, JetifierExtension::class.java, project)
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierTask.kt b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierTask.kt
new file mode 100644
index 0000000..d518f7b
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/src/main/kotlin/android/support/tools/jetifier/plugin/gradle/JetifierTask.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.plugin.gradle
+
+import android.support.tools.jetifier.core.Processor
+import android.support.tools.jetifier.core.config.ConfigParser
+import android.support.tools.jetifier.core.utils.Log
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFiles
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+
+/**
+ * The jetifier task that is run by gradle.
+ */
+open class JetifierTask : DefaultTask() {
+
+ companion object {
+ const val OUTPUT_DIR_APPENDIX = "jetifier"
+ const val GROUP_ID = "Pre-build"
+ // TODO: Get back to this once the name of the library is decided.
+ const val DESCRIPTION = "Rewrites input libraries to run with jetpack"
+ }
+
+ private var inputFiles = project.files()
+ private val outputDir = File(project.buildDir, OUTPUT_DIR_APPENDIX)
+
+ override fun getGroup() = GROUP_ID
+
+ override fun getDescription() = DESCRIPTION
+
+ fun addFilesToProcess(files: FileCollection) {
+ inputFiles = project.files(inputFiles.files.plus(files))
+ }
+
+ @InputFiles
+ fun getInputFiles(): FileCollection {
+ return inputFiles
+ }
+
+ @OutputFiles
+ fun getOutputFiles(): FileCollection {
+ return project.files(inputFiles.map { File(outputDir, it.name) }.toList())
+ }
+
+ @TaskAction
+ @Throws(Exception::class)
+ fun run() {
+ // Hook to the gradle logger
+ Log.logConsumer = JetifierLoggerAdapter(logger)
+
+ val config = ConfigParser.loadDefaultConfig()
+ ?: throw RuntimeException("Failed to load the default config!")
+
+ val processor = Processor(config)
+
+ for(inputFile in inputFiles) {
+ val inputPath = inputFile.toPath()
+ processor.transform(listOf(inputPath), outputDir.toPath())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle-plugin/src/main/resources/META-INF/gradle-plugins/android.jetifier.properties b/jetifier/jetifier/gradle-plugin/src/main/resources/META-INF/gradle-plugins/android.jetifier.properties
new file mode 100644
index 0000000..7ee7e73
--- /dev/null
+++ b/jetifier/jetifier/gradle-plugin/src/main/resources/META-INF/gradle-plugins/android.jetifier.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+#
+
+implementation-class=android.support.tools.jetifier.plugin.gradle.JetifierPlugin
\ No newline at end of file
diff --git a/jetifier/jetifier/gradle/wrapper/gradle-wrapper.jar b/jetifier/jetifier/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..2411732
--- /dev/null
+++ b/jetifier/jetifier/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/jetifier/jetifier/gradle/wrapper/gradle-wrapper.properties b/jetifier/jetifier/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2d19ff3
--- /dev/null
+++ b/jetifier/jetifier/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=../../../../../../tools/external/gradle/gradle-4.1-bin.zip
diff --git a/jetifier/jetifier/gradlew b/jetifier/jetifier/gradlew
new file mode 100755
index 0000000..04fca86
--- /dev/null
+++ b/jetifier/jetifier/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when runTransform from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/jetifier/jetifier/gradlew.bat b/jetifier/jetifier/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/jetifier/jetifier/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/jetifier/jetifier/preprocessor/build.gradle b/jetifier/jetifier/preprocessor/build.gradle
new file mode 100644
index 0000000..b688a9d
--- /dev/null
+++ b/jetifier/jetifier/preprocessor/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+version '1.0'
+
+apply plugin: "application"
+
+mainClassName = "android.support.tools.jetifier.preprocessor.MainKt"
+
+dependencies {
+ compile project(':core')
+ compile group: 'commons-cli', name: 'commons-cli', version: '1.3.1'
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/preprocessor/src/main/kotlin/android/support/tools/jetifier/preprocessor/Main.kt b/jetifier/jetifier/preprocessor/src/main/kotlin/android/support/tools/jetifier/preprocessor/Main.kt
new file mode 100644
index 0000000..2313115
--- /dev/null
+++ b/jetifier/jetifier/preprocessor/src/main/kotlin/android/support/tools/jetifier/preprocessor/Main.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.preprocessor
+
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.config.ConfigParser
+import android.support.tools.jetifier.core.map.LibraryMapGenerator
+import android.support.tools.jetifier.core.utils.Log
+import org.apache.commons.cli.*
+import java.nio.file.Path
+
+import java.nio.file.Paths
+
+class Main {
+
+ companion object {
+ const val TAG = "Main"
+ const val TOOL_NAME = "preprocessor"
+
+ val OPTIONS = Options()
+ val OPTION_INPUT_LIBS = createOption("i", "Input libraries paths", multiple = true)
+ val OPTION_INPUT_CONFIG = createOption("c", "Input config path")
+ val OPTION_OUTPUT_CONFIG = createOption("o", "Output config path")
+ val OPTION_LOG_LEVEL = createOption("l", "Logging level. debug, verbose, default",
+ isRequired = false)
+
+ private fun createOption(argName: String,
+ desc: String,
+ isRequired: Boolean = true,
+ multiple: Boolean = false) : Option {
+ val op = Option(argName, true, desc)
+ op.isRequired = isRequired
+ if (multiple) {
+ op.args = Option.UNLIMITED_VALUES
+ }
+ OPTIONS.addOption(op)
+ return op
+ }
+ }
+
+ fun run(args : Array<String>) {
+ val cmd = parseCmdLine(args)
+ if (cmd == null) {
+ System.exit(1)
+ return
+ }
+
+ Log.setLevel(cmd.getOptionValue(OPTION_LOG_LEVEL.opt))
+
+ val inputLibraries = cmd.getOptionValues(OPTION_INPUT_LIBS.opt).map { Paths.get(it) }
+ val inputConfigPath = Paths.get(cmd.getOptionValue(OPTION_INPUT_CONFIG.opt))
+ val outputConfigPath = Paths.get(cmd.getOptionValue(OPTION_OUTPUT_CONFIG.opt))
+
+ val config = ConfigParser.loadFromFile(inputConfigPath)
+ if (config == null) {
+ System.exit(1)
+ return
+ }
+
+ generateMapping(config, inputLibraries, outputConfigPath)
+ }
+
+ private fun parseCmdLine(args : Array<String>) : CommandLine? {
+ try {
+ return DefaultParser().parse(OPTIONS, args)
+ } catch (e: ParseException) {
+ Log.e(TAG, e.message.orEmpty())
+ HelpFormatter().printHelp(TOOL_NAME, OPTIONS)
+ }
+ return null
+ }
+
+ private fun generateMapping(config: Config, inputLibraries: List<Path>, outputConfigPath: Path) {
+ val mapper = LibraryMapGenerator(config)
+ inputLibraries.forEach {
+ val library = Archive.Builder.extract(it)
+ mapper.scanLibrary(library)
+ }
+
+ val map = mapper.generateMap()
+ val newConfig = config.setNewMap(map)
+ ConfigParser.writeToFile(newConfig, outputConfigPath)
+ }
+
+}
+
+
+fun main(args : Array<String>) {
+ Main().run(args)
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/settings.gradle b/jetifier/jetifier/settings.gradle
new file mode 100644
index 0000000..7d13b73
--- /dev/null
+++ b/jetifier/jetifier/settings.gradle
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+rootProject.name = 'jetifier'
+include 'core'
+include 'gradle-plugin'
+include 'standalone'
+include 'preprocessor'
+
diff --git a/jetifier/jetifier/standalone/build.gradle b/jetifier/jetifier/standalone/build.gradle
new file mode 100644
index 0000000..8814d50
--- /dev/null
+++ b/jetifier/jetifier/standalone/build.gradle
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+version '1.0'
+
+apply plugin: "application"
+
+mainClassName = "android.support.tools.jetifier.standalone.MainKt"
+
+dependencies {
+ compile project(':core')
+ compile group: 'commons-cli', name: 'commons-cli', version: '1.3.1'
+}
+
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
new file mode 100644
index 0000000..70cf52d
--- /dev/null
+++ b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.tools.jetifier.standalone
+
+import android.support.tools.jetifier.core.Processor
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.config.ConfigParser
+import android.support.tools.jetifier.core.utils.Log
+import org.apache.commons.cli.*
+
+import java.nio.file.Paths
+
+class Main {
+
+ companion object {
+ const val TAG = "Main"
+ const val TOOL_NAME = "standalone"
+
+ val OPTIONS = Options()
+ val OPTION_INPUT = createOption("i", "Input libraries paths", multiple = true)
+ val OPTION_OUTPUT = createOption("o", "Output config path")
+ val OPTION_CONFIG = createOption("c", "Input config path", isRequired = false)
+ val OPTION_LOG_LEVEL = createOption("l", "Logging level. debug, verbose, default",
+ isRequired = false)
+
+ private fun createOption(argName: String,
+ desc: String,
+ isRequired: Boolean = true,
+ multiple: Boolean = false) : Option {
+ val op = Option(argName, true, desc)
+ op.isRequired = isRequired
+ if (multiple) {
+ op.args = Option.UNLIMITED_VALUES
+ }
+ OPTIONS.addOption(op)
+ return op
+ }
+ }
+
+ fun run(args : Array<String>) {
+ val cmd = parseCmdLine(args)
+ if (cmd == null) {
+ System.exit(1)
+ return
+ }
+
+ Log.setLevel(cmd.getOptionValue(OPTION_LOG_LEVEL.opt))
+
+ val inputLibraries = cmd.getOptionValues(OPTION_INPUT.opt).map { Paths.get(it) }
+ val outputPath = Paths.get(cmd.getOptionValue(OPTION_OUTPUT.opt))
+
+ val config : Config?
+ if (cmd.hasOption(OPTION_CONFIG.opt)) {
+ val configPath = Paths.get(cmd.getOptionValue(OPTION_CONFIG.opt))
+ config = ConfigParser.loadFromFile(configPath)
+ } else {
+ config = ConfigParser.loadDefaultConfig()
+ }
+
+ if (config == null) {
+ Log.e(TAG, "Failed to load the config file")
+ System.exit(1)
+ return
+ }
+
+ val processor = Processor(config)
+ processor.transform(inputLibraries, outputPath)
+ }
+
+ private fun parseCmdLine(args : Array<String>) : CommandLine? {
+ try {
+ return DefaultParser().parse(OPTIONS, args)
+ } catch (e: ParseException) {
+ Log.e(TAG, e.message.orEmpty())
+ HelpFormatter().printHelp(TOOL_NAME, OPTIONS)
+ }
+ return null
+ }
+
+}
+
+
+fun main(args : Array<String>) {
+ Main().run(args)
+}
\ No newline at end of file
diff --git a/v17/leanback/Android.mk b/leanback/Android.mk
similarity index 100%
rename from v17/leanback/Android.mk
rename to leanback/Android.mk
diff --git a/v17/leanback/AndroidManifest.xml b/leanback/AndroidManifest.xml
similarity index 100%
rename from v17/leanback/AndroidManifest.xml
rename to leanback/AndroidManifest.xml
diff --git a/v17/leanback/OWNERS b/leanback/OWNERS
similarity index 100%
rename from v17/leanback/OWNERS
rename to leanback/OWNERS
diff --git a/v17/leanback/api/26.0.0.ignore b/leanback/api/26.0.0.ignore
similarity index 100%
rename from v17/leanback/api/26.0.0.ignore
rename to leanback/api/26.0.0.ignore
diff --git a/v17/leanback/api/26.0.0.txt b/leanback/api/26.0.0.txt
similarity index 100%
rename from v17/leanback/api/26.0.0.txt
rename to leanback/api/26.0.0.txt
diff --git a/v17/leanback/api/26.1.0.ignore b/leanback/api/26.1.0.ignore
similarity index 100%
rename from v17/leanback/api/26.1.0.ignore
rename to leanback/api/26.1.0.ignore
diff --git a/v17/leanback/api/26.1.0.txt b/leanback/api/26.1.0.txt
similarity index 100%
rename from v17/leanback/api/26.1.0.txt
rename to leanback/api/26.1.0.txt
diff --git a/v17/leanback/api/27.0.0.txt b/leanback/api/27.0.0.txt
similarity index 100%
rename from v17/leanback/api/27.0.0.txt
rename to leanback/api/27.0.0.txt
diff --git a/leanback/api/current.txt b/leanback/api/current.txt
new file mode 100644
index 0000000..4a5067c
--- /dev/null
+++ b/leanback/api/current.txt
@@ -0,0 +1,3138 @@
+package android.support.v17.leanback.app {
+
+ public final class BackgroundManager {
+ method public void attach(android.view.Window);
+ method public void attachToView(android.view.View);
+ method public void clearDrawable();
+ method public final int getColor();
+ method public deprecated android.graphics.drawable.Drawable getDefaultDimLayer();
+ method public deprecated android.graphics.drawable.Drawable getDimLayer();
+ method public android.graphics.drawable.Drawable getDrawable();
+ method public static android.support.v17.leanback.app.BackgroundManager getInstance(android.app.Activity);
+ method public boolean isAttached();
+ method public boolean isAutoReleaseOnStop();
+ method public void release();
+ method public void setAutoReleaseOnStop(boolean);
+ method public void setBitmap(android.graphics.Bitmap);
+ method public void setColor(int);
+ method public deprecated void setDimLayer(android.graphics.drawable.Drawable);
+ method public void setDrawable(android.graphics.drawable.Drawable);
+ method public void setThemeDrawableResourceId(int);
+ }
+
+ public deprecated class BaseFragment extends android.support.v17.leanback.app.BrandedFragment {
+ method protected java.lang.Object createEntranceTransition();
+ method public final android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
+ method protected void onEntranceTransitionEnd();
+ method protected void onEntranceTransitionPrepare();
+ method protected void onEntranceTransitionStart();
+ method public void prepareEntranceTransition();
+ method protected void runEntranceTransition(java.lang.Object);
+ method public void startEntranceTransition();
+ }
+
+ abstract deprecated class BaseRowFragment extends android.app.Fragment {
+ method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
+ method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
+ method public int getSelectedPosition();
+ method public final android.support.v17.leanback.widget.VerticalGridView getVerticalGridView();
+ method public void onTransitionEnd();
+ method public boolean onTransitionPrepare();
+ method public void onTransitionStart();
+ method public final void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setAlignment(int);
+ method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ }
+
+ abstract class BaseRowSupportFragment extends android.support.v4.app.Fragment {
+ method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
+ method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
+ method public int getSelectedPosition();
+ method public final android.support.v17.leanback.widget.VerticalGridView getVerticalGridView();
+ method public void onTransitionEnd();
+ method public boolean onTransitionPrepare();
+ method public void onTransitionStart();
+ method public final void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setAlignment(int);
+ method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ }
+
+ public class BaseSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
+ method protected java.lang.Object createEntranceTransition();
+ method public final android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
+ method protected void onEntranceTransitionEnd();
+ method protected void onEntranceTransitionPrepare();
+ method protected void onEntranceTransitionStart();
+ method public void prepareEntranceTransition();
+ method protected void runEntranceTransition(java.lang.Object);
+ method public void startEntranceTransition();
+ }
+
+ public deprecated class BrandedFragment extends android.app.Fragment {
+ ctor public BrandedFragment();
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public int getSearchAffordanceColor();
+ method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
+ method public java.lang.CharSequence getTitle();
+ method public android.view.View getTitleView();
+ method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
+ method public void installTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public final boolean isShowingTitle();
+ method public android.view.View onInflateTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnSearchClickedListener(android.view.View.OnClickListener);
+ method public void setSearchAffordanceColor(int);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setTitle(java.lang.CharSequence);
+ method public void setTitleView(android.view.View);
+ method public void showTitle(boolean);
+ method public void showTitle(int);
+ }
+
+ public class BrandedSupportFragment extends android.support.v4.app.Fragment {
+ ctor public BrandedSupportFragment();
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public int getSearchAffordanceColor();
+ method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
+ method public java.lang.CharSequence getTitle();
+ method public android.view.View getTitleView();
+ method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
+ method public void installTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public final boolean isShowingTitle();
+ method public android.view.View onInflateTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnSearchClickedListener(android.view.View.OnClickListener);
+ method public void setSearchAffordanceColor(int);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setTitle(java.lang.CharSequence);
+ method public void setTitleView(android.view.View);
+ method public void showTitle(boolean);
+ method public void showTitle(int);
+ }
+
+ public deprecated class BrowseFragment extends android.support.v17.leanback.app.BaseFragment {
+ ctor public BrowseFragment();
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, int);
+ method public void enableMainFragmentScaling(boolean);
+ method public deprecated void enableRowScaling(boolean);
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public int getBrandColor();
+ method public android.support.v17.leanback.app.HeadersFragment getHeadersFragment();
+ method public int getHeadersState();
+ method public android.app.Fragment getMainFragment();
+ method public final android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterRegistry getMainFragmentRegistry();
+ method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
+ method public int getSelectedPosition();
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getSelectedRowViewHolder();
+ method public final boolean isHeadersTransitionOnBackEnabled();
+ method public boolean isInHeadersTransition();
+ method public boolean isShowingHeaders();
+ method public android.support.v17.leanback.app.HeadersFragment onCreateHeadersFragment();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setBrandColor(int);
+ method public void setBrowseTransitionListener(android.support.v17.leanback.app.BrowseFragment.BrowseTransitionListener);
+ method public void setHeaderPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public void setHeadersState(int);
+ method public final void setHeadersTransitionOnBackEnabled(boolean);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ method public void startHeadersTransition(boolean);
+ field public static final int HEADERS_DISABLED = 3; // 0x3
+ field public static final int HEADERS_ENABLED = 1; // 0x1
+ field public static final int HEADERS_HIDDEN = 2; // 0x2
+ }
+
+ public static deprecated class BrowseFragment.BrowseTransitionListener {
+ ctor public BrowseFragment.BrowseTransitionListener();
+ method public void onHeadersTransitionStart(boolean);
+ method public void onHeadersTransitionStop(boolean);
+ }
+
+ public static abstract deprecated class BrowseFragment.FragmentFactory<T extends android.app.Fragment> {
+ ctor public BrowseFragment.FragmentFactory();
+ method public abstract T createFragment(java.lang.Object);
+ }
+
+ public static abstract deprecated interface BrowseFragment.FragmentHost {
+ method public abstract void notifyDataReady(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
+ method public abstract void notifyViewCreated(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
+ method public abstract void showTitleView(boolean);
+ }
+
+ public static deprecated class BrowseFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseFragment.FragmentFactory {
+ ctor public BrowseFragment.ListRowFragmentFactory();
+ method public android.support.v17.leanback.app.RowsFragment createFragment(java.lang.Object);
+ }
+
+ public static deprecated class BrowseFragment.MainFragmentAdapter<T extends android.app.Fragment> {
+ ctor public BrowseFragment.MainFragmentAdapter(T);
+ method public final T getFragment();
+ method public final android.support.v17.leanback.app.BrowseFragment.FragmentHost getFragmentHost();
+ method public boolean isScalingEnabled();
+ method public boolean isScrolling();
+ method public void onTransitionEnd();
+ method public boolean onTransitionPrepare();
+ method public void onTransitionStart();
+ method public void setAlignment(int);
+ method public void setEntranceTransitionState(boolean);
+ method public void setExpand(boolean);
+ method public void setScalingEnabled(boolean);
+ }
+
+ public static abstract deprecated interface BrowseFragment.MainFragmentAdapterProvider {
+ method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter getMainFragmentAdapter();
+ }
+
+ public static final deprecated class BrowseFragment.MainFragmentAdapterRegistry {
+ ctor public BrowseFragment.MainFragmentAdapterRegistry();
+ method public android.app.Fragment createFragment(java.lang.Object);
+ method public void registerFragment(java.lang.Class, android.support.v17.leanback.app.BrowseFragment.FragmentFactory);
+ }
+
+ public static deprecated class BrowseFragment.MainFragmentRowsAdapter<T extends android.app.Fragment> {
+ ctor public BrowseFragment.MainFragmentRowsAdapter(T);
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
+ method public final T getFragment();
+ method public int getSelectedPosition();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ method public void setSelectedPosition(int, boolean);
+ }
+
+ public static abstract deprecated interface BrowseFragment.MainFragmentRowsAdapterProvider {
+ method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ }
+
+ public class BrowseSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
+ ctor public BrowseSupportFragment();
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, int);
+ method public void enableMainFragmentScaling(boolean);
+ method public deprecated void enableRowScaling(boolean);
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public int getBrandColor();
+ method public int getHeadersState();
+ method public android.support.v17.leanback.app.HeadersSupportFragment getHeadersSupportFragment();
+ method public android.support.v4.app.Fragment getMainFragment();
+ method public final android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapterRegistry getMainFragmentRegistry();
+ method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
+ method public int getSelectedPosition();
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getSelectedRowViewHolder();
+ method public final boolean isHeadersTransitionOnBackEnabled();
+ method public boolean isInHeadersTransition();
+ method public boolean isShowingHeaders();
+ method public android.support.v17.leanback.app.HeadersSupportFragment onCreateHeadersSupportFragment();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setBrandColor(int);
+ method public void setBrowseTransitionListener(android.support.v17.leanback.app.BrowseSupportFragment.BrowseTransitionListener);
+ method public void setHeaderPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public void setHeadersState(int);
+ method public final void setHeadersTransitionOnBackEnabled(boolean);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ method public void startHeadersTransition(boolean);
+ field public static final int HEADERS_DISABLED = 3; // 0x3
+ field public static final int HEADERS_ENABLED = 1; // 0x1
+ field public static final int HEADERS_HIDDEN = 2; // 0x2
+ }
+
+ public static class BrowseSupportFragment.BrowseTransitionListener {
+ ctor public BrowseSupportFragment.BrowseTransitionListener();
+ method public void onHeadersTransitionStart(boolean);
+ method public void onHeadersTransitionStop(boolean);
+ }
+
+ public static abstract class BrowseSupportFragment.FragmentFactory<T extends android.support.v4.app.Fragment> {
+ ctor public BrowseSupportFragment.FragmentFactory();
+ method public abstract T createFragment(java.lang.Object);
+ }
+
+ public static abstract interface BrowseSupportFragment.FragmentHost {
+ method public abstract void notifyDataReady(android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter);
+ method public abstract void notifyViewCreated(android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter);
+ method public abstract void showTitleView(boolean);
+ }
+
+ public static class BrowseSupportFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseSupportFragment.FragmentFactory {
+ ctor public BrowseSupportFragment.ListRowFragmentFactory();
+ method public android.support.v17.leanback.app.RowsSupportFragment createFragment(java.lang.Object);
+ }
+
+ public static class BrowseSupportFragment.MainFragmentAdapter<T extends android.support.v4.app.Fragment> {
+ ctor public BrowseSupportFragment.MainFragmentAdapter(T);
+ method public final T getFragment();
+ method public final android.support.v17.leanback.app.BrowseSupportFragment.FragmentHost getFragmentHost();
+ method public boolean isScalingEnabled();
+ method public boolean isScrolling();
+ method public void onTransitionEnd();
+ method public boolean onTransitionPrepare();
+ method public void onTransitionStart();
+ method public void setAlignment(int);
+ method public void setEntranceTransitionState(boolean);
+ method public void setExpand(boolean);
+ method public void setScalingEnabled(boolean);
+ }
+
+ public static abstract interface BrowseSupportFragment.MainFragmentAdapterProvider {
+ method public abstract android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter();
+ }
+
+ public static final class BrowseSupportFragment.MainFragmentAdapterRegistry {
+ ctor public BrowseSupportFragment.MainFragmentAdapterRegistry();
+ method public android.support.v4.app.Fragment createFragment(java.lang.Object);
+ method public void registerFragment(java.lang.Class, android.support.v17.leanback.app.BrowseSupportFragment.FragmentFactory);
+ }
+
+ public static class BrowseSupportFragment.MainFragmentRowsAdapter<T extends android.support.v4.app.Fragment> {
+ ctor public BrowseSupportFragment.MainFragmentRowsAdapter(T);
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
+ method public final T getFragment();
+ method public int getSelectedPosition();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ method public void setSelectedPosition(int, boolean);
+ }
+
+ public static abstract interface BrowseSupportFragment.MainFragmentRowsAdapterProvider {
+ method public abstract android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ }
+
+ public deprecated class DetailsFragment extends android.support.v17.leanback.app.BaseFragment {
+ ctor public DetailsFragment();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.DetailsParallax getParallax();
+ method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
+ method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method protected void onSetDetailsOverviewRowStatus(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int);
+ method protected void onSetRowStatus(android.support.v17.leanback.widget.RowPresenter, android.support.v17.leanback.widget.RowPresenter.ViewHolder, int, int, int);
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method protected void setupDetailsOverviewRowPresenter(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
+ method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
+ }
+
+ public deprecated class DetailsFragmentBackgroundController {
+ ctor public DetailsFragmentBackgroundController(android.support.v17.leanback.app.DetailsFragment);
+ method public boolean canNavigateToVideoFragment();
+ method public void enableParallax();
+ method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+ method public final android.app.Fragment findOrCreateVideoFragment();
+ method public final android.graphics.drawable.Drawable getBottomDrawable();
+ method public final android.graphics.Bitmap getCoverBitmap();
+ method public final android.graphics.drawable.Drawable getCoverDrawable();
+ method public final int getParallaxDrawableMaxOffset();
+ method public final android.support.v17.leanback.media.PlaybackGlue getPlaybackGlue();
+ method public final int getSolidColor();
+ method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+ method public android.app.Fragment onCreateVideoFragment();
+ method public final void setCoverBitmap(android.graphics.Bitmap);
+ method public final void setParallaxDrawableMaxOffset(int);
+ method public final void setSolidColor(int);
+ method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+ method public final void switchToRows();
+ method public final void switchToVideo();
+ }
+
+ public class DetailsSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
+ ctor public DetailsSupportFragment();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.DetailsParallax getParallax();
+ method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
+ method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method protected void onSetDetailsOverviewRowStatus(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int);
+ method protected void onSetRowStatus(android.support.v17.leanback.widget.RowPresenter, android.support.v17.leanback.widget.RowPresenter.ViewHolder, int, int, int);
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method protected void setupDetailsOverviewRowPresenter(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
+ method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
+ }
+
+ public class DetailsSupportFragmentBackgroundController {
+ ctor public DetailsSupportFragmentBackgroundController(android.support.v17.leanback.app.DetailsSupportFragment);
+ method public boolean canNavigateToVideoSupportFragment();
+ method public void enableParallax();
+ method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
+ method public final android.support.v4.app.Fragment findOrCreateVideoSupportFragment();
+ method public final android.graphics.drawable.Drawable getBottomDrawable();
+ method public final android.graphics.Bitmap getCoverBitmap();
+ method public final android.graphics.drawable.Drawable getCoverDrawable();
+ method public final int getParallaxDrawableMaxOffset();
+ method public final android.support.v17.leanback.media.PlaybackGlue getPlaybackGlue();
+ method public final int getSolidColor();
+ method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
+ method public android.support.v4.app.Fragment onCreateVideoSupportFragment();
+ method public final void setCoverBitmap(android.graphics.Bitmap);
+ method public final void setParallaxDrawableMaxOffset(int);
+ method public final void setSolidColor(int);
+ method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
+ method public final void switchToRows();
+ method public final void switchToVideo();
+ }
+
+ public deprecated class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
+ ctor public ErrorFragment();
+ method public android.graphics.drawable.Drawable getBackgroundDrawable();
+ method public android.view.View.OnClickListener getButtonClickListener();
+ method public java.lang.String getButtonText();
+ method public android.graphics.drawable.Drawable getImageDrawable();
+ method public java.lang.CharSequence getMessage();
+ method public boolean isBackgroundTranslucent();
+ method public void setBackgroundDrawable(android.graphics.drawable.Drawable);
+ method public void setButtonClickListener(android.view.View.OnClickListener);
+ method public void setButtonText(java.lang.String);
+ method public void setDefaultBackground(boolean);
+ method public void setImageDrawable(android.graphics.drawable.Drawable);
+ method public void setMessage(java.lang.CharSequence);
+ }
+
+ public class ErrorSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
+ ctor public ErrorSupportFragment();
+ method public android.graphics.drawable.Drawable getBackgroundDrawable();
+ method public android.view.View.OnClickListener getButtonClickListener();
+ method public java.lang.String getButtonText();
+ method public android.graphics.drawable.Drawable getImageDrawable();
+ method public java.lang.CharSequence getMessage();
+ method public boolean isBackgroundTranslucent();
+ method public void setBackgroundDrawable(android.graphics.drawable.Drawable);
+ method public void setButtonClickListener(android.view.View.OnClickListener);
+ method public void setButtonText(java.lang.String);
+ method public void setDefaultBackground(boolean);
+ method public void setImageDrawable(android.graphics.drawable.Drawable);
+ method public void setMessage(java.lang.CharSequence);
+ }
+
+ public deprecated class GuidedStepFragment extends android.app.Fragment {
+ ctor public GuidedStepFragment();
+ method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment);
+ method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment, int);
+ method public static int addAsRoot(android.app.Activity, android.support.v17.leanback.app.GuidedStepFragment, int);
+ method public void collapseAction(boolean);
+ method public void collapseSubActions();
+ method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
+ method public void expandSubActions(android.support.v17.leanback.widget.GuidedAction);
+ method public android.support.v17.leanback.widget.GuidedAction findActionById(long);
+ method public int findActionPositionById(long);
+ method public android.support.v17.leanback.widget.GuidedAction findButtonActionById(long);
+ method public int findButtonActionPositionById(long);
+ method public void finishGuidedStepFragments();
+ method public android.view.View getActionItemView(int);
+ method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getActions();
+ method public android.view.View getButtonActionItemView(int);
+ method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getButtonActions();
+ method public static android.support.v17.leanback.app.GuidedStepFragment getCurrentGuidedStepFragment(android.app.FragmentManager);
+ method public android.support.v17.leanback.widget.GuidanceStylist getGuidanceStylist();
+ method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedActionsStylist();
+ method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedButtonActionsStylist();
+ method public int getSelectedActionPosition();
+ method public int getSelectedButtonActionPosition();
+ method public int getUiStyle();
+ method public boolean isExpanded();
+ method public boolean isFocusOutEndAllowed();
+ method public boolean isFocusOutStartAllowed();
+ method public boolean isSubActionsExpanded();
+ method public void notifyActionChanged(int);
+ method public void notifyButtonActionChanged(int);
+ method protected void onAddSharedElementTransition(android.app.FragmentTransaction, android.support.v17.leanback.app.GuidedStepFragment);
+ method public void onCreateActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateActionsStylist();
+ method public android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public void onCreateButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateButtonActionsStylist();
+ method public android.support.v17.leanback.widget.GuidanceStylist.Guidance onCreateGuidance(android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidanceStylist onCreateGuidanceStylist();
+ method public void onGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
+ method public void onGuidedActionEditCanceled(android.support.v17.leanback.widget.GuidedAction);
+ method public deprecated void onGuidedActionEdited(android.support.v17.leanback.widget.GuidedAction);
+ method public long onGuidedActionEditedAndProceed(android.support.v17.leanback.widget.GuidedAction);
+ method public void onGuidedActionFocused(android.support.v17.leanback.widget.GuidedAction);
+ method protected void onProvideFragmentTransitions();
+ method public int onProvideTheme();
+ method public boolean onSubGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
+ method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
+ method public void popBackStackToGuidedStepFragment(java.lang.Class, int);
+ method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setActionsDiffCallback(android.support.v17.leanback.widget.DiffCallback<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setSelectedActionPosition(int);
+ method public void setSelectedButtonActionPosition(int);
+ method public void setUiStyle(int);
+ field public static final java.lang.String EXTRA_UI_STYLE = "uiStyle";
+ field public static final int UI_STYLE_ACTIVITY_ROOT = 2; // 0x2
+ field public static final deprecated int UI_STYLE_DEFAULT = 0; // 0x0
+ field public static final int UI_STYLE_ENTRANCE = 1; // 0x1
+ field public static final int UI_STYLE_REPLACE = 0; // 0x0
+ }
+
+ public class GuidedStepSupportFragment extends android.support.v4.app.Fragment {
+ ctor public GuidedStepSupportFragment();
+ method public static int add(android.support.v4.app.FragmentManager, android.support.v17.leanback.app.GuidedStepSupportFragment);
+ method public static int add(android.support.v4.app.FragmentManager, android.support.v17.leanback.app.GuidedStepSupportFragment, int);
+ method public static int addAsRoot(android.support.v4.app.FragmentActivity, android.support.v17.leanback.app.GuidedStepSupportFragment, int);
+ method public void collapseAction(boolean);
+ method public void collapseSubActions();
+ method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
+ method public void expandSubActions(android.support.v17.leanback.widget.GuidedAction);
+ method public android.support.v17.leanback.widget.GuidedAction findActionById(long);
+ method public int findActionPositionById(long);
+ method public android.support.v17.leanback.widget.GuidedAction findButtonActionById(long);
+ method public int findButtonActionPositionById(long);
+ method public void finishGuidedStepSupportFragments();
+ method public android.view.View getActionItemView(int);
+ method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getActions();
+ method public android.view.View getButtonActionItemView(int);
+ method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getButtonActions();
+ method public static android.support.v17.leanback.app.GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(android.support.v4.app.FragmentManager);
+ method public android.support.v17.leanback.widget.GuidanceStylist getGuidanceStylist();
+ method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedActionsStylist();
+ method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedButtonActionsStylist();
+ method public int getSelectedActionPosition();
+ method public int getSelectedButtonActionPosition();
+ method public int getUiStyle();
+ method public boolean isExpanded();
+ method public boolean isFocusOutEndAllowed();
+ method public boolean isFocusOutStartAllowed();
+ method public boolean isSubActionsExpanded();
+ method public void notifyActionChanged(int);
+ method public void notifyButtonActionChanged(int);
+ method protected void onAddSharedElementTransition(android.support.v4.app.FragmentTransaction, android.support.v17.leanback.app.GuidedStepSupportFragment);
+ method public void onCreateActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateActionsStylist();
+ method public android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
+ method public void onCreateButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateButtonActionsStylist();
+ method public android.support.v17.leanback.widget.GuidanceStylist.Guidance onCreateGuidance(android.os.Bundle);
+ method public android.support.v17.leanback.widget.GuidanceStylist onCreateGuidanceStylist();
+ method public void onGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
+ method public void onGuidedActionEditCanceled(android.support.v17.leanback.widget.GuidedAction);
+ method public deprecated void onGuidedActionEdited(android.support.v17.leanback.widget.GuidedAction);
+ method public long onGuidedActionEditedAndProceed(android.support.v17.leanback.widget.GuidedAction);
+ method public void onGuidedActionFocused(android.support.v17.leanback.widget.GuidedAction);
+ method protected void onProvideFragmentTransitions();
+ method public int onProvideTheme();
+ method public boolean onSubGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
+ method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
+ method public void popBackStackToGuidedStepSupportFragment(java.lang.Class, int);
+ method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setActionsDiffCallback(android.support.v17.leanback.widget.DiffCallback<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setSelectedActionPosition(int);
+ method public void setSelectedButtonActionPosition(int);
+ method public void setUiStyle(int);
+ field public static final java.lang.String EXTRA_UI_STYLE = "uiStyle";
+ field public static final int UI_STYLE_ACTIVITY_ROOT = 2; // 0x2
+ field public static final deprecated int UI_STYLE_DEFAULT = 0; // 0x0
+ field public static final int UI_STYLE_ENTRANCE = 1; // 0x1
+ field public static final int UI_STYLE_REPLACE = 0; // 0x0
+ }
+
+ public deprecated class HeadersFragment extends android.support.v17.leanback.app.BaseRowFragment {
+ ctor public HeadersFragment();
+ method public boolean isScrolling();
+ method public void setOnHeaderClickedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderClickedListener);
+ method public void setOnHeaderViewSelectedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderViewSelectedListener);
+ }
+
+ public static abstract deprecated interface HeadersFragment.OnHeaderClickedListener {
+ method public abstract void onHeaderClicked(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
+ }
+
+ public static abstract deprecated interface HeadersFragment.OnHeaderViewSelectedListener {
+ method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
+ }
+
+ public class HeadersSupportFragment extends android.support.v17.leanback.app.BaseRowSupportFragment {
+ ctor public HeadersSupportFragment();
+ method public boolean isScrolling();
+ method public void setOnHeaderClickedListener(android.support.v17.leanback.app.HeadersSupportFragment.OnHeaderClickedListener);
+ method public void setOnHeaderViewSelectedListener(android.support.v17.leanback.app.HeadersSupportFragment.OnHeaderViewSelectedListener);
+ }
+
+ public static abstract interface HeadersSupportFragment.OnHeaderClickedListener {
+ method public abstract void onHeaderClicked(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
+ }
+
+ public static abstract interface HeadersSupportFragment.OnHeaderViewSelectedListener {
+ method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
+ }
+
+ public abstract deprecated class OnboardingFragment extends android.app.Fragment {
+ ctor public OnboardingFragment();
+ method public final int getArrowBackgroundColor();
+ method public final int getArrowColor();
+ method protected final int getCurrentPageIndex();
+ method public final int getDescriptionViewTextColor();
+ method public final int getDotBackgroundColor();
+ method public final int getIconResourceId();
+ method public final int getLogoResourceId();
+ method protected abstract int getPageCount();
+ method protected abstract java.lang.CharSequence getPageDescription(int);
+ method protected abstract java.lang.CharSequence getPageTitle(int);
+ method public final java.lang.CharSequence getStartButtonText();
+ method public final int getTitleViewTextColor();
+ method protected final boolean isLogoAnimationFinished();
+ method protected void moveToNextPage();
+ method protected void moveToPreviousPage();
+ method protected abstract android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected abstract android.view.View onCreateContentView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected android.animation.Animator onCreateDescriptionAnimator();
+ method protected android.animation.Animator onCreateEnterAnimation();
+ method protected abstract android.view.View onCreateForegroundView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected android.animation.Animator onCreateLogoAnimation();
+ method protected android.animation.Animator onCreateTitleAnimator();
+ method protected void onFinishFragment();
+ method protected void onLogoAnimationFinished();
+ method protected void onPageChanged(int, int);
+ method public int onProvideTheme();
+ method public void setArrowBackgroundColor(int);
+ method public void setArrowColor(int);
+ method public void setDescriptionViewTextColor(int);
+ method public void setDotBackgroundColor(int);
+ method public final void setIconResouceId(int);
+ method public final void setLogoResourceId(int);
+ method public void setStartButtonText(java.lang.CharSequence);
+ method public void setTitleViewTextColor(int);
+ method protected final void startEnterAnimation(boolean);
+ }
+
+ public abstract class OnboardingSupportFragment extends android.support.v4.app.Fragment {
+ ctor public OnboardingSupportFragment();
+ method public final int getArrowBackgroundColor();
+ method public final int getArrowColor();
+ method protected final int getCurrentPageIndex();
+ method public final int getDescriptionViewTextColor();
+ method public final int getDotBackgroundColor();
+ method public final int getIconResourceId();
+ method public final int getLogoResourceId();
+ method protected abstract int getPageCount();
+ method protected abstract java.lang.CharSequence getPageDescription(int);
+ method protected abstract java.lang.CharSequence getPageTitle(int);
+ method public final java.lang.CharSequence getStartButtonText();
+ method public final int getTitleViewTextColor();
+ method protected final boolean isLogoAnimationFinished();
+ method protected void moveToNextPage();
+ method protected void moveToPreviousPage();
+ method protected abstract android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected abstract android.view.View onCreateContentView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected android.animation.Animator onCreateDescriptionAnimator();
+ method protected android.animation.Animator onCreateEnterAnimation();
+ method protected abstract android.view.View onCreateForegroundView(android.view.LayoutInflater, android.view.ViewGroup);
+ method protected android.animation.Animator onCreateLogoAnimation();
+ method protected android.animation.Animator onCreateTitleAnimator();
+ method protected void onFinishFragment();
+ method protected void onLogoAnimationFinished();
+ method protected void onPageChanged(int, int);
+ method public int onProvideTheme();
+ method public void setArrowBackgroundColor(int);
+ method public void setArrowColor(int);
+ method public void setDescriptionViewTextColor(int);
+ method public void setDotBackgroundColor(int);
+ method public final void setIconResouceId(int);
+ method public final void setLogoResourceId(int);
+ method public void setStartButtonText(java.lang.CharSequence);
+ method public void setTitleViewTextColor(int);
+ method protected final void startEnterAnimation(boolean);
+ }
+
+ public deprecated class PlaybackFragment extends android.app.Fragment {
+ ctor public PlaybackFragment();
+ method public deprecated void fadeOut();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public int getBackgroundType();
+ method public android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
+ method public void hideControlsOverlay(boolean);
+ method public boolean isControlsOverlayAutoHideEnabled();
+ method public boolean isControlsOverlayVisible();
+ method public deprecated boolean isFadingEnabled();
+ method public void notifyPlaybackRowChanged();
+ method protected void onBufferingStateChanged(boolean);
+ method protected void onError(int, java.lang.CharSequence);
+ method protected void onVideoSizeChanged(int, int);
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setBackgroundType(int);
+ method public void setControlsOverlayAutoHideEnabled(boolean);
+ method public deprecated void setFadingEnabled(boolean);
+ method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
+ method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
+ method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method public void showControlsOverlay(boolean);
+ method public void tickle();
+ field public static final int BG_DARK = 1; // 0x1
+ field public static final int BG_LIGHT = 2; // 0x2
+ field public static final int BG_NONE = 0; // 0x0
+ }
+
+ public deprecated class PlaybackFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
+ ctor public PlaybackFragmentGlueHost(android.support.v17.leanback.app.PlaybackFragment);
+ method public void fadeOut();
+ method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ }
+
+ public class PlaybackSupportFragment extends android.support.v4.app.Fragment {
+ ctor public PlaybackSupportFragment();
+ method public deprecated void fadeOut();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public int getBackgroundType();
+ method public android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
+ method public void hideControlsOverlay(boolean);
+ method public boolean isControlsOverlayAutoHideEnabled();
+ method public boolean isControlsOverlayVisible();
+ method public deprecated boolean isFadingEnabled();
+ method public void notifyPlaybackRowChanged();
+ method protected void onBufferingStateChanged(boolean);
+ method protected void onError(int, java.lang.CharSequence);
+ method protected void onVideoSizeChanged(int, int);
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setBackgroundType(int);
+ method public void setControlsOverlayAutoHideEnabled(boolean);
+ method public deprecated void setFadingEnabled(boolean);
+ method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
+ method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
+ method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, boolean);
+ method public void showControlsOverlay(boolean);
+ method public void tickle();
+ field public static final int BG_DARK = 1; // 0x1
+ field public static final int BG_LIGHT = 2; // 0x2
+ field public static final int BG_NONE = 0; // 0x0
+ }
+
+ public class PlaybackSupportFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
+ ctor public PlaybackSupportFragmentGlueHost(android.support.v17.leanback.app.PlaybackSupportFragment);
+ method public void fadeOut();
+ method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ }
+
+ public final class ProgressBarManager {
+ ctor public ProgressBarManager();
+ method public void disableProgressBar();
+ method public void enableProgressBar();
+ method public long getInitialDelay();
+ method public void hide();
+ method public void setInitialDelay(long);
+ method public void setProgressBarView(android.view.View);
+ method public void setRootView(android.view.ViewGroup);
+ method public void show();
+ }
+
+ public deprecated class RowsFragment extends android.support.v17.leanback.app.BaseRowFragment implements android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapterProvider {
+ ctor public RowsFragment();
+ method public deprecated void enableRowScaling(boolean);
+ method protected android.support.v17.leanback.widget.VerticalGridView findGridViewFromRoot(android.view.View);
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
+ method public android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter getMainFragmentAdapter();
+ method public android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(int);
+ method public boolean isScrolling();
+ method public void setEntranceTransitionState(boolean);
+ method public void setExpand(boolean);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ }
+
+ public static class RowsFragment.MainFragmentAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter {
+ ctor public RowsFragment.MainFragmentAdapter(android.support.v17.leanback.app.RowsFragment);
+ }
+
+ public static deprecated class RowsFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter {
+ ctor public RowsFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsFragment);
+ }
+
+ public class RowsSupportFragment extends android.support.v17.leanback.app.BaseRowSupportFragment implements android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapterProvider {
+ ctor public RowsSupportFragment();
+ method public deprecated void enableRowScaling(boolean);
+ method protected android.support.v17.leanback.widget.VerticalGridView findGridViewFromRoot(android.view.View);
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
+ method public android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter();
+ method public android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+ method public android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(int);
+ method public boolean isScrolling();
+ method public void setEntranceTransitionState(boolean);
+ method public void setExpand(boolean);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ }
+
+ public static class RowsSupportFragment.MainFragmentAdapter extends android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter {
+ ctor public RowsSupportFragment.MainFragmentAdapter(android.support.v17.leanback.app.RowsSupportFragment);
+ }
+
+ public static class RowsSupportFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter {
+ ctor public RowsSupportFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsSupportFragment);
+ }
+
+ public deprecated class SearchFragment extends android.app.Fragment {
+ ctor public SearchFragment();
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String);
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, java.lang.String);
+ method public void displayCompletions(java.util.List<java.lang.String>);
+ method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public android.content.Intent getRecognizerIntent();
+ method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
+ method public java.lang.String getTitle();
+ method public static android.support.v17.leanback.app.SearchFragment newInstance(java.lang.String);
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchQuery(java.lang.String, boolean);
+ method public void setSearchQuery(android.content.Intent, boolean);
+ method public void setSearchResultProvider(android.support.v17.leanback.app.SearchFragment.SearchResultProvider);
+ method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
+ method public void setTitle(java.lang.String);
+ method public void startRecognition();
+ }
+
+ public static abstract interface SearchFragment.SearchResultProvider {
+ method public abstract android.support.v17.leanback.widget.ObjectAdapter getResultsAdapter();
+ method public abstract boolean onQueryTextChange(java.lang.String);
+ method public abstract boolean onQueryTextSubmit(java.lang.String);
+ }
+
+ public class SearchSupportFragment extends android.support.v4.app.Fragment {
+ ctor public SearchSupportFragment();
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String);
+ method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, java.lang.String);
+ method public void displayCompletions(java.util.List<java.lang.String>);
+ method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public android.content.Intent getRecognizerIntent();
+ method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
+ method public java.lang.String getTitle();
+ method public static android.support.v17.leanback.app.SearchSupportFragment newInstance(java.lang.String);
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchQuery(java.lang.String, boolean);
+ method public void setSearchQuery(android.content.Intent, boolean);
+ method public void setSearchResultProvider(android.support.v17.leanback.app.SearchSupportFragment.SearchResultProvider);
+ method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
+ method public void setTitle(java.lang.String);
+ method public void startRecognition();
+ }
+
+ public static abstract interface SearchSupportFragment.SearchResultProvider {
+ method public abstract android.support.v17.leanback.widget.ObjectAdapter getResultsAdapter();
+ method public abstract boolean onQueryTextChange(java.lang.String);
+ method public abstract boolean onQueryTextSubmit(java.lang.String);
+ }
+
+ public deprecated class VerticalGridFragment extends android.support.v17.leanback.app.BaseFragment {
+ ctor public VerticalGridFragment();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public android.support.v17.leanback.widget.VerticalGridPresenter getGridPresenter();
+ method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setGridPresenter(android.support.v17.leanback.widget.VerticalGridPresenter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ }
+
+ public class VerticalGridSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
+ ctor public VerticalGridSupportFragment();
+ method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public android.support.v17.leanback.widget.VerticalGridPresenter getGridPresenter();
+ method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setGridPresenter(android.support.v17.leanback.widget.VerticalGridPresenter);
+ method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public void setSelectedPosition(int);
+ }
+
+ public deprecated class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
+ ctor public VideoFragment();
+ method public android.view.SurfaceView getSurfaceView();
+ method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+ }
+
+ public deprecated class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+ ctor public VideoFragmentGlueHost(android.support.v17.leanback.app.VideoFragment);
+ method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+ }
+
+ public class VideoSupportFragment extends android.support.v17.leanback.app.PlaybackSupportFragment {
+ ctor public VideoSupportFragment();
+ method public android.view.SurfaceView getSurfaceView();
+ method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+ }
+
+ public class VideoSupportFragmentGlueHost extends android.support.v17.leanback.app.PlaybackSupportFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+ ctor public VideoSupportFragmentGlueHost(android.support.v17.leanback.app.VideoSupportFragment);
+ method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+ }
+
+}
+
+package android.support.v17.leanback.database {
+
+ public abstract class CursorMapper {
+ ctor public CursorMapper();
+ method protected abstract java.lang.Object bind(android.database.Cursor);
+ method protected abstract void bindColumns(android.database.Cursor);
+ method public java.lang.Object convert(android.database.Cursor);
+ }
+
+}
+
+package android.support.v17.leanback.graphics {
+
+ public class BoundsRule {
+ ctor public BoundsRule();
+ ctor public BoundsRule(android.support.v17.leanback.graphics.BoundsRule);
+ method public void calculateBounds(android.graphics.Rect, android.graphics.Rect);
+ field public android.support.v17.leanback.graphics.BoundsRule.ValueRule bottom;
+ field public android.support.v17.leanback.graphics.BoundsRule.ValueRule left;
+ field public android.support.v17.leanback.graphics.BoundsRule.ValueRule right;
+ field public android.support.v17.leanback.graphics.BoundsRule.ValueRule top;
+ }
+
+ public static final class BoundsRule.ValueRule {
+ method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule absoluteValue(int);
+ method public int getAbsoluteValue();
+ method public float getFraction();
+ method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParent(float);
+ method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParentWithOffset(float, int);
+ method public void setAbsoluteValue(int);
+ method public void setFraction(float);
+ }
+
+ public final class ColorFilterCache {
+ method public static android.support.v17.leanback.graphics.ColorFilterCache getColorFilterCache(int);
+ method public android.graphics.ColorFilter getFilterForLevel(float);
+ }
+
+ public final class ColorFilterDimmer {
+ method public void applyFilterToView(android.view.View);
+ method public static android.support.v17.leanback.graphics.ColorFilterDimmer create(android.support.v17.leanback.graphics.ColorFilterCache, float, float);
+ method public static android.support.v17.leanback.graphics.ColorFilterDimmer createDefault(android.content.Context);
+ method public android.graphics.ColorFilter getColorFilter();
+ method public android.graphics.Paint getPaint();
+ method public void setActiveLevel(float);
+ }
+
+ public final class ColorOverlayDimmer {
+ method public int applyToColor(int);
+ method public static android.support.v17.leanback.graphics.ColorOverlayDimmer createColorOverlayDimmer(int, float, float);
+ method public static android.support.v17.leanback.graphics.ColorOverlayDimmer createDefault(android.content.Context);
+ method public void drawColorOverlay(android.graphics.Canvas, android.view.View, boolean);
+ method public int getAlpha();
+ method public float getAlphaFloat();
+ method public android.graphics.Paint getPaint();
+ method public boolean needsDraw();
+ method public void setActiveLevel(float);
+ }
+
+ public class CompositeDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
+ ctor public CompositeDrawable();
+ method public void addChildDrawable(android.graphics.drawable.Drawable);
+ method public void draw(android.graphics.Canvas);
+ method public android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable getChildAt(int);
+ method public int getChildCount();
+ method public android.graphics.drawable.Drawable getDrawable(int);
+ method public int getOpacity();
+ method public void invalidateDrawable(android.graphics.drawable.Drawable);
+ method public void removeChild(int);
+ method public void removeDrawable(android.graphics.drawable.Drawable);
+ method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long);
+ method public void setAlpha(int);
+ method public void setChildDrawableAt(int, android.graphics.drawable.Drawable);
+ method public void setColorFilter(android.graphics.ColorFilter);
+ method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
+ }
+
+ public static final class CompositeDrawable.ChildDrawable {
+ ctor public CompositeDrawable.ChildDrawable(android.graphics.drawable.Drawable, android.support.v17.leanback.graphics.CompositeDrawable);
+ method public android.support.v17.leanback.graphics.BoundsRule getBoundsRule();
+ method public android.graphics.drawable.Drawable getDrawable();
+ method public void recomputeBounds();
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> BOTTOM_ABSOLUTE;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> BOTTOM_FRACTION;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> LEFT_ABSOLUTE;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> LEFT_FRACTION;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> RIGHT_ABSOLUTE;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> RIGHT_FRACTION;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> TOP_ABSOLUTE;
+ field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> TOP_FRACTION;
+ }
+
+ public class FitWidthBitmapDrawable extends android.graphics.drawable.Drawable {
+ ctor public FitWidthBitmapDrawable();
+ method public void draw(android.graphics.Canvas);
+ method public android.graphics.Bitmap getBitmap();
+ method public int getOpacity();
+ method public android.graphics.Rect getSource();
+ method public int getVerticalOffset();
+ method public void setAlpha(int);
+ method public void setBitmap(android.graphics.Bitmap);
+ method public void setColorFilter(android.graphics.ColorFilter);
+ method public void setSource(android.graphics.Rect);
+ method public void setVerticalOffset(int);
+ field public static final android.util.Property<android.support.v17.leanback.graphics.FitWidthBitmapDrawable, java.lang.Integer> PROPERTY_VERTICAL_OFFSET;
+ }
+
+}
+
+package android.support.v17.leanback.media {
+
+ public class MediaControllerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
+ ctor public MediaControllerAdapter(android.support.v4.media.session.MediaControllerCompat);
+ method public android.graphics.drawable.Drawable getMediaArt(android.content.Context);
+ method public android.support.v4.media.session.MediaControllerCompat getMediaController();
+ method public java.lang.CharSequence getMediaSubtitle();
+ method public java.lang.CharSequence getMediaTitle();
+ method public void pause();
+ method public void play();
+ }
+
+ public abstract deprecated class MediaControllerGlue extends android.support.v17.leanback.media.PlaybackControlGlue {
+ ctor public MediaControllerGlue(android.content.Context, int[], int[]);
+ method public void attachToMediaController(android.support.v4.media.session.MediaControllerCompat);
+ method public void detach();
+ method public int getCurrentPosition();
+ method public int getCurrentSpeedId();
+ method public android.graphics.drawable.Drawable getMediaArt();
+ method public final android.support.v4.media.session.MediaControllerCompat getMediaController();
+ method public int getMediaDuration();
+ method public java.lang.CharSequence getMediaSubtitle();
+ method public java.lang.CharSequence getMediaTitle();
+ method public long getSupportedActions();
+ method public boolean hasValidMedia();
+ method public boolean isMediaPlaying();
+ }
+
+ public class MediaPlayerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
+ ctor public MediaPlayerAdapter(android.content.Context);
+ method protected boolean onError(int, int);
+ method protected boolean onInfo(int, int);
+ method protected void onSeekComplete();
+ method public void pause();
+ method public void play();
+ method public void release();
+ method public void reset();
+ method public boolean setDataSource(android.net.Uri);
+ }
+
+ public class PlaybackBannerControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackBaseControlGlue {
+ ctor public PlaybackBannerControlGlue(android.content.Context, int[], T);
+ ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T);
+ method public int[] getFastForwardSpeeds();
+ method public int[] getRewindSpeeds();
+ method public void onActionClicked(android.support.v17.leanback.widget.Action);
+ method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
+ method public boolean onKey(android.view.View, int, android.view.KeyEvent);
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
+ field public static final int PLAYBACK_SPEED_FAST_L0 = 10; // 0xa
+ field public static final int PLAYBACK_SPEED_FAST_L1 = 11; // 0xb
+ field public static final int PLAYBACK_SPEED_FAST_L2 = 12; // 0xc
+ field public static final int PLAYBACK_SPEED_FAST_L3 = 13; // 0xd
+ field public static final int PLAYBACK_SPEED_FAST_L4 = 14; // 0xe
+ field public static final int PLAYBACK_SPEED_INVALID = -1; // 0xffffffff
+ field public static final int PLAYBACK_SPEED_NORMAL = 1; // 0x1
+ field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
+ }
+
+ public abstract class PlaybackBaseControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
+ ctor public PlaybackBaseControlGlue(android.content.Context, T);
+ method public android.graphics.drawable.Drawable getArt();
+ method public final long getBufferedPosition();
+ method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
+ method public long getCurrentPosition();
+ method public final long getDuration();
+ method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
+ method public final T getPlayerAdapter();
+ method public java.lang.CharSequence getSubtitle();
+ method public long getSupportedActions();
+ method public java.lang.CharSequence getTitle();
+ method public boolean isControlsOverlayAutoHideEnabled();
+ method public final boolean isPlaying();
+ method public final boolean isPrepared();
+ method protected static void notifyItemChanged(android.support.v17.leanback.widget.ArrayObjectAdapter, java.lang.Object);
+ method public abstract void onActionClicked(android.support.v17.leanback.widget.Action);
+ method protected void onCreatePrimaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
+ method protected abstract android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
+ method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
+ method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method protected void onMetadataChanged();
+ method protected void onPlayCompleted();
+ method protected void onPlayStateChanged();
+ method protected void onPreparedStateChanged();
+ method protected void onUpdateBufferedProgress();
+ method protected void onUpdateDuration();
+ method protected void onUpdateProgress();
+ method public final void seekTo(long);
+ method public void setArt(android.graphics.drawable.Drawable);
+ method public void setControlsOverlayAutoHideEnabled(boolean);
+ method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
+ method public void setSubtitle(java.lang.CharSequence);
+ method public void setTitle(java.lang.CharSequence);
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REPEAT = 512; // 0x200
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SHUFFLE = 1024; // 0x400
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
+ }
+
+ public abstract class PlaybackControlGlue extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
+ ctor public PlaybackControlGlue(android.content.Context, int[]);
+ ctor public PlaybackControlGlue(android.content.Context, int[], int[]);
+ method public void enableProgressUpdating(boolean);
+ method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
+ method public deprecated android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
+ method public abstract int getCurrentPosition();
+ method public abstract int getCurrentSpeedId();
+ method public int[] getFastForwardSpeeds();
+ method public abstract android.graphics.drawable.Drawable getMediaArt();
+ method public abstract int getMediaDuration();
+ method public abstract java.lang.CharSequence getMediaSubtitle();
+ method public abstract java.lang.CharSequence getMediaTitle();
+ method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
+ method public int[] getRewindSpeeds();
+ method public abstract long getSupportedActions();
+ method public int getUpdatePeriod();
+ method public abstract boolean hasValidMedia();
+ method public boolean isFadingEnabled();
+ method public abstract boolean isMediaPlaying();
+ method public void onActionClicked(android.support.v17.leanback.widget.Action);
+ method protected void onCreateControlsRowAndPresenter();
+ method protected void onCreatePrimaryActions(android.support.v17.leanback.widget.SparseArrayObjectAdapter);
+ method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
+ method public boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method protected void onMetadataChanged();
+ method protected void onStateChanged();
+ method public void play(int);
+ method public final void play();
+ method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
+ method public deprecated void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
+ method public void setFadingEnabled(boolean);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
+ method public void updateProgress();
+ field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
+ field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
+ field public static final int ACTION_FAST_FORWARD = 128; // 0x80
+ field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
+ field public static final int ACTION_REWIND = 32; // 0x20
+ field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
+ field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
+ field public static final int PLAYBACK_SPEED_FAST_L0 = 10; // 0xa
+ field public static final int PLAYBACK_SPEED_FAST_L1 = 11; // 0xb
+ field public static final int PLAYBACK_SPEED_FAST_L2 = 12; // 0xc
+ field public static final int PLAYBACK_SPEED_FAST_L3 = 13; // 0xd
+ field public static final int PLAYBACK_SPEED_FAST_L4 = 14; // 0xe
+ field public static final int PLAYBACK_SPEED_INVALID = -1; // 0xffffffff
+ field public static final int PLAYBACK_SPEED_NORMAL = 1; // 0x1
+ field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
+ }
+
+ public abstract class PlaybackGlue {
+ ctor public PlaybackGlue(android.content.Context);
+ method public void addPlayerCallback(android.support.v17.leanback.media.PlaybackGlue.PlayerCallback);
+ method public android.content.Context getContext();
+ method public android.support.v17.leanback.media.PlaybackGlueHost getHost();
+ method protected java.util.List<android.support.v17.leanback.media.PlaybackGlue.PlayerCallback> getPlayerCallbacks();
+ method public boolean isPlaying();
+ method public boolean isPrepared();
+ method public void next();
+ method protected void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
+ method protected void onDetachedFromHost();
+ method protected void onHostPause();
+ method protected void onHostResume();
+ method protected void onHostStart();
+ method protected void onHostStop();
+ method public void pause();
+ method public void play();
+ method public void playWhenPrepared();
+ method public void previous();
+ method public void removePlayerCallback(android.support.v17.leanback.media.PlaybackGlue.PlayerCallback);
+ method public final void setHost(android.support.v17.leanback.media.PlaybackGlueHost);
+ }
+
+ public static abstract class PlaybackGlue.PlayerCallback {
+ ctor public PlaybackGlue.PlayerCallback();
+ method public void onPlayCompleted(android.support.v17.leanback.media.PlaybackGlue);
+ method public void onPlayStateChanged(android.support.v17.leanback.media.PlaybackGlue);
+ method public void onPreparedStateChanged(android.support.v17.leanback.media.PlaybackGlue);
+ }
+
+ public abstract class PlaybackGlueHost {
+ ctor public PlaybackGlueHost();
+ method public deprecated void fadeOut();
+ method public android.support.v17.leanback.media.PlaybackGlueHost.PlayerCallback getPlayerCallback();
+ method public void hideControlsOverlay(boolean);
+ method public boolean isControlsOverlayAutoHideEnabled();
+ method public boolean isControlsOverlayVisible();
+ method public void notifyPlaybackRowChanged();
+ method public void setControlsOverlayAutoHideEnabled(boolean);
+ method public deprecated void setFadingEnabled(boolean);
+ method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
+ method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
+ method public void setOnKeyInterceptListener(android.view.View.OnKeyListener);
+ method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
+ method public void showControlsOverlay(boolean);
+ }
+
+ public static abstract class PlaybackGlueHost.HostCallback {
+ ctor public PlaybackGlueHost.HostCallback();
+ method public void onHostDestroy();
+ method public void onHostPause();
+ method public void onHostResume();
+ method public void onHostStart();
+ method public void onHostStop();
+ }
+
+ public static class PlaybackGlueHost.PlayerCallback {
+ ctor public PlaybackGlueHost.PlayerCallback();
+ method public void onBufferingStateChanged(boolean);
+ method public void onError(int, java.lang.CharSequence);
+ method public void onVideoSizeChanged(int, int);
+ }
+
+ public class PlaybackTransportControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackBaseControlGlue {
+ ctor public PlaybackTransportControlGlue(android.content.Context, T);
+ method public final android.support.v17.leanback.widget.PlaybackSeekDataProvider getSeekProvider();
+ method public final boolean isSeekEnabled();
+ method public void onActionClicked(android.support.v17.leanback.widget.Action);
+ method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
+ method public boolean onKey(android.view.View, int, android.view.KeyEvent);
+ method public final void setSeekEnabled(boolean);
+ method public final void setSeekProvider(android.support.v17.leanback.widget.PlaybackSeekDataProvider);
+ }
+
+ public abstract class PlayerAdapter {
+ ctor public PlayerAdapter();
+ method public void fastForward();
+ method public long getBufferedPosition();
+ method public final android.support.v17.leanback.media.PlayerAdapter.Callback getCallback();
+ method public long getCurrentPosition();
+ method public long getDuration();
+ method public long getSupportedActions();
+ method public boolean isPlaying();
+ method public boolean isPrepared();
+ method public void next();
+ method public void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
+ method public void onDetachedFromHost();
+ method public abstract void pause();
+ method public abstract void play();
+ method public void previous();
+ method public void rewind();
+ method public void seekTo(long);
+ method public final void setCallback(android.support.v17.leanback.media.PlayerAdapter.Callback);
+ method public void setProgressUpdatingEnabled(boolean);
+ method public void setRepeatAction(int);
+ method public void setShuffleAction(int);
+ }
+
+ public static class PlayerAdapter.Callback {
+ ctor public PlayerAdapter.Callback();
+ method public void onBufferedPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onBufferingStateChanged(android.support.v17.leanback.media.PlayerAdapter, boolean);
+ method public void onCurrentPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onDurationChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onError(android.support.v17.leanback.media.PlayerAdapter, int, java.lang.String);
+ method public void onMetadataChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onPlayCompleted(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onPlayStateChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onPreparedStateChanged(android.support.v17.leanback.media.PlayerAdapter);
+ method public void onVideoSizeChanged(android.support.v17.leanback.media.PlayerAdapter, int, int);
+ }
+
+ public abstract interface SurfaceHolderGlueHost {
+ method public abstract void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
+ }
+
+}
+
+package android.support.v17.leanback.system {
+
+ public class Settings {
+ method public boolean getBoolean(java.lang.String);
+ method public static android.support.v17.leanback.system.Settings getInstance(android.content.Context);
+ method public void setBoolean(java.lang.String, boolean);
+ field public static final java.lang.String OUTLINE_CLIPPING_DISABLED = "OUTLINE_CLIPPING_DISABLED";
+ field public static final java.lang.String PREFER_STATIC_SHADOWS = "PREFER_STATIC_SHADOWS";
+ }
+
+}
+
+package android.support.v17.leanback.widget {
+
+ public abstract class AbstractDetailsDescriptionPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public AbstractDetailsDescriptionPresenter();
+ method protected abstract void onBindDescription(android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter.ViewHolder, java.lang.Object);
+ method public final void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public final android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ }
+
+ public static class AbstractDetailsDescriptionPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
+ ctor public AbstractDetailsDescriptionPresenter.ViewHolder(android.view.View);
+ method public android.widget.TextView getBody();
+ method public android.widget.TextView getSubtitle();
+ method public android.widget.TextView getTitle();
+ }
+
+ public abstract class AbstractMediaItemPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public AbstractMediaItemPresenter();
+ ctor public AbstractMediaItemPresenter(int);
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method public android.support.v17.leanback.widget.Presenter getActionPresenter();
+ method protected int getMediaPlayState(java.lang.Object);
+ method public int getThemeId();
+ method public boolean hasMediaRowSeparator();
+ method protected abstract void onBindMediaDetails(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder, java.lang.Object);
+ method public void onBindMediaPlayState(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
+ method protected void onBindRowActions(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
+ method protected void onUnbindMediaDetails(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
+ method public void onUnbindMediaPlayState(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
+ method public void setActionPresenter(android.support.v17.leanback.widget.Presenter);
+ method public void setBackgroundColor(int);
+ method public void setHasMediaRowSeparator(boolean);
+ method public void setThemeId(int);
+ field public static final int PLAY_STATE_INITIAL = 0; // 0x0
+ field public static final int PLAY_STATE_PAUSED = 1; // 0x1
+ field public static final int PLAY_STATE_PLAYING = 2; // 0x2
+ }
+
+ public static class AbstractMediaItemPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public AbstractMediaItemPresenter.ViewHolder(android.view.View);
+ method public android.view.ViewGroup getMediaItemActionsContainer();
+ method public android.view.View getMediaItemDetailsView();
+ method public android.widget.TextView getMediaItemDurationView();
+ method public android.widget.TextView getMediaItemNameView();
+ method public android.widget.TextView getMediaItemNumberView();
+ method public android.widget.ViewFlipper getMediaItemNumberViewFlipper();
+ method public android.view.View getMediaItemPausedView();
+ method public android.view.View getMediaItemPlayingView();
+ method public android.support.v17.leanback.widget.MultiActionsProvider.MultiAction[] getMediaItemRowActions();
+ method public android.view.View getMediaItemRowSeparator();
+ method public android.view.View getSelectorView();
+ method public void notifyActionChanged(android.support.v17.leanback.widget.MultiActionsProvider.MultiAction);
+ method public void notifyDetailsChanged();
+ method public void notifyPlayStateChanged();
+ method public void onBindRowActions();
+ method public void setSelectedMediaItemNumberView(int);
+ }
+
+ public abstract class AbstractMediaListHeaderPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public AbstractMediaListHeaderPresenter(android.content.Context, int);
+ ctor public AbstractMediaListHeaderPresenter();
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method protected abstract void onBindMediaListHeaderViewHolder(android.support.v17.leanback.widget.AbstractMediaListHeaderPresenter.ViewHolder, java.lang.Object);
+ method public void setBackgroundColor(int);
+ }
+
+ public static class AbstractMediaListHeaderPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public AbstractMediaListHeaderPresenter.ViewHolder(android.view.View);
+ method public android.widget.TextView getHeaderView();
+ }
+
+ public class Action {
+ ctor public Action(long);
+ ctor public Action(long, java.lang.CharSequence);
+ ctor public Action(long, java.lang.CharSequence, java.lang.CharSequence);
+ ctor public Action(long, java.lang.CharSequence, java.lang.CharSequence, android.graphics.drawable.Drawable);
+ method public final void addKeyCode(int);
+ method public final android.graphics.drawable.Drawable getIcon();
+ method public final long getId();
+ method public final java.lang.CharSequence getLabel1();
+ method public final java.lang.CharSequence getLabel2();
+ method public final void removeKeyCode(int);
+ method public final boolean respondsToKeyCode(int);
+ method public final void setIcon(android.graphics.drawable.Drawable);
+ method public final void setId(long);
+ method public final void setLabel1(java.lang.CharSequence);
+ method public final void setLabel2(java.lang.CharSequence);
+ field public static final long NO_ID = -1L; // 0xffffffffffffffffL
+ }
+
+ public class ArrayObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
+ ctor public ArrayObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
+ ctor public ArrayObjectAdapter(android.support.v17.leanback.widget.Presenter);
+ ctor public ArrayObjectAdapter();
+ method public void add(java.lang.Object);
+ method public void add(int, java.lang.Object);
+ method public void addAll(int, java.util.Collection);
+ method public void clear();
+ method public java.lang.Object get(int);
+ method public int indexOf(java.lang.Object);
+ method public void move(int, int);
+ method public void notifyArrayItemRangeChanged(int, int);
+ method public boolean remove(java.lang.Object);
+ method public int removeItems(int, int);
+ method public void replace(int, java.lang.Object);
+ method public void setItems(java.util.List, android.support.v17.leanback.widget.DiffCallback);
+ method public int size();
+ method public <E> java.util.List<E> unmodifiableList();
+ }
+
+ public class BaseCardView extends android.widget.FrameLayout {
+ ctor public BaseCardView(android.content.Context);
+ ctor public BaseCardView(android.content.Context, android.util.AttributeSet);
+ ctor public BaseCardView(android.content.Context, android.util.AttributeSet, int);
+ method protected android.support.v17.leanback.widget.BaseCardView.LayoutParams generateDefaultLayoutParams();
+ method public android.support.v17.leanback.widget.BaseCardView.LayoutParams generateLayoutParams(android.util.AttributeSet);
+ method protected android.support.v17.leanback.widget.BaseCardView.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
+ method public int getCardType();
+ method public deprecated int getExtraVisibility();
+ method public int getInfoVisibility();
+ method public boolean isSelectedAnimationDelayed();
+ method public void setCardType(int);
+ method public deprecated void setExtraVisibility(int);
+ method public void setInfoVisibility(int);
+ method public void setSelectedAnimationDelayed(boolean);
+ field public static final int CARD_REGION_VISIBLE_ACTIVATED = 1; // 0x1
+ field public static final int CARD_REGION_VISIBLE_ALWAYS = 0; // 0x0
+ field public static final int CARD_REGION_VISIBLE_SELECTED = 2; // 0x2
+ field public static final int CARD_TYPE_INFO_OVER = 1; // 0x1
+ field public static final int CARD_TYPE_INFO_UNDER = 2; // 0x2
+ field public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3; // 0x3
+ field public static final int CARD_TYPE_MAIN_ONLY = 0; // 0x0
+ }
+
+ public static class BaseCardView.LayoutParams extends android.widget.FrameLayout.LayoutParams {
+ ctor public BaseCardView.LayoutParams(android.content.Context, android.util.AttributeSet);
+ ctor public BaseCardView.LayoutParams(int, int);
+ ctor public BaseCardView.LayoutParams(android.view.ViewGroup.LayoutParams);
+ ctor public BaseCardView.LayoutParams(android.support.v17.leanback.widget.BaseCardView.LayoutParams);
+ field public static final int VIEW_TYPE_EXTRA = 2; // 0x2
+ field public static final int VIEW_TYPE_INFO = 1; // 0x1
+ field public static final int VIEW_TYPE_MAIN = 0; // 0x0
+ field public int viewType;
+ }
+
+ public abstract class BaseGridView extends android.support.v7.widget.RecyclerView {
+ method public void addOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
+ method public void animateIn();
+ method public void animateOut();
+ method public int getChildDrawingOrder(int, int);
+ method public deprecated int getHorizontalMargin();
+ method public int getHorizontalSpacing();
+ method public int getInitialPrefetchItemCount();
+ method public int getItemAlignmentOffset();
+ method public float getItemAlignmentOffsetPercent();
+ method public int getItemAlignmentViewId();
+ method public android.support.v17.leanback.widget.BaseGridView.OnUnhandledKeyListener getOnUnhandledKeyListener();
+ method public final int getSaveChildrenLimitNumber();
+ method public final int getSaveChildrenPolicy();
+ method public int getSelectedPosition();
+ method public deprecated int getVerticalMargin();
+ method public int getVerticalSpacing();
+ method public void getViewSelectedOffsets(android.view.View, int[]);
+ method public int getWindowAlignment();
+ method public int getWindowAlignmentOffset();
+ method public float getWindowAlignmentOffsetPercent();
+ method public boolean hasPreviousViewInSameRow(int);
+ method public boolean isChildLayoutAnimated();
+ method public boolean isFocusDrawingOrderEnabled();
+ method public final boolean isFocusSearchDisabled();
+ method public boolean isItemAlignmentOffsetWithPadding();
+ method public boolean isScrollEnabled();
+ method public boolean isWindowAlignmentPreferKeyLineOverHighEdge();
+ method public boolean isWindowAlignmentPreferKeyLineOverLowEdge();
+ method public boolean onRequestFocusInDescendants(int, android.graphics.Rect);
+ method public void removeOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
+ method public void setAnimateChildLayout(boolean);
+ method public void setChildrenVisibility(int);
+ method public void setFocusDrawingOrderEnabled(boolean);
+ method public final void setFocusSearchDisabled(boolean);
+ method public void setGravity(int);
+ method public void setHasOverlappingRendering(boolean);
+ method public deprecated void setHorizontalMargin(int);
+ method public void setHorizontalSpacing(int);
+ method public void setInitialPrefetchItemCount(int);
+ method public void setItemAlignmentOffset(int);
+ method public void setItemAlignmentOffsetPercent(float);
+ method public void setItemAlignmentOffsetWithPadding(boolean);
+ method public void setItemAlignmentViewId(int);
+ method public deprecated void setItemMargin(int);
+ method public void setItemSpacing(int);
+ method public void setLayoutEnabled(boolean);
+ method public void setOnChildLaidOutListener(android.support.v17.leanback.widget.OnChildLaidOutListener);
+ method public void setOnChildSelectedListener(android.support.v17.leanback.widget.OnChildSelectedListener);
+ method public void setOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
+ method public void setOnKeyInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnKeyInterceptListener);
+ method public void setOnMotionInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnMotionInterceptListener);
+ method public void setOnTouchInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnTouchInterceptListener);
+ method public void setOnUnhandledKeyListener(android.support.v17.leanback.widget.BaseGridView.OnUnhandledKeyListener);
+ method public void setPruneChild(boolean);
+ method public final void setSaveChildrenLimitNumber(int);
+ method public final void setSaveChildrenPolicy(int);
+ method public void setScrollEnabled(boolean);
+ method public void setSelectedPosition(int);
+ method public void setSelectedPosition(int, int);
+ method public void setSelectedPosition(int, android.support.v17.leanback.widget.ViewHolderTask);
+ method public void setSelectedPositionSmooth(int);
+ method public void setSelectedPositionSmooth(int, android.support.v17.leanback.widget.ViewHolderTask);
+ method public deprecated void setVerticalMargin(int);
+ method public void setVerticalSpacing(int);
+ method public void setWindowAlignment(int);
+ method public void setWindowAlignmentOffset(int);
+ method public void setWindowAlignmentOffsetPercent(float);
+ method public void setWindowAlignmentPreferKeyLineOverHighEdge(boolean);
+ method public void setWindowAlignmentPreferKeyLineOverLowEdge(boolean);
+ field public static final float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
+ field public static final int SAVE_ALL_CHILD = 3; // 0x3
+ field public static final int SAVE_LIMITED_CHILD = 2; // 0x2
+ field public static final int SAVE_NO_CHILD = 0; // 0x0
+ field public static final int SAVE_ON_SCREEN_CHILD = 1; // 0x1
+ field public static final int WINDOW_ALIGN_BOTH_EDGE = 3; // 0x3
+ field public static final int WINDOW_ALIGN_HIGH_EDGE = 2; // 0x2
+ field public static final int WINDOW_ALIGN_LOW_EDGE = 1; // 0x1
+ field public static final int WINDOW_ALIGN_NO_EDGE = 0; // 0x0
+ field public static final float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
+ }
+
+ public static abstract interface BaseGridView.OnKeyInterceptListener {
+ method public abstract boolean onInterceptKeyEvent(android.view.KeyEvent);
+ }
+
+ public static abstract interface BaseGridView.OnMotionInterceptListener {
+ method public abstract boolean onInterceptMotionEvent(android.view.MotionEvent);
+ }
+
+ public static abstract interface BaseGridView.OnTouchInterceptListener {
+ method public abstract boolean onInterceptTouchEvent(android.view.MotionEvent);
+ }
+
+ public static abstract interface BaseGridView.OnUnhandledKeyListener {
+ method public abstract boolean onUnhandledKey(android.view.KeyEvent);
+ }
+
+ public abstract interface BaseOnItemViewClickedListener<T> {
+ method public abstract void onItemClicked(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, android.support.v17.leanback.widget.RowPresenter.ViewHolder, T);
+ }
+
+ public abstract interface BaseOnItemViewSelectedListener<T> {
+ method public abstract void onItemSelected(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, android.support.v17.leanback.widget.RowPresenter.ViewHolder, T);
+ }
+
+ public class BrowseFrameLayout extends android.widget.FrameLayout {
+ ctor public BrowseFrameLayout(android.content.Context);
+ ctor public BrowseFrameLayout(android.content.Context, android.util.AttributeSet);
+ ctor public BrowseFrameLayout(android.content.Context, android.util.AttributeSet, int);
+ method public android.support.v17.leanback.widget.BrowseFrameLayout.OnChildFocusListener getOnChildFocusListener();
+ method public android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener getOnFocusSearchListener();
+ method public void setOnChildFocusListener(android.support.v17.leanback.widget.BrowseFrameLayout.OnChildFocusListener);
+ method public void setOnDispatchKeyListener(android.view.View.OnKeyListener);
+ method public void setOnFocusSearchListener(android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener);
+ }
+
+ public static abstract interface BrowseFrameLayout.OnChildFocusListener {
+ method public abstract void onRequestChildFocus(android.view.View, android.view.View);
+ method public abstract boolean onRequestFocusInDescendants(int, android.graphics.Rect);
+ }
+
+ public static abstract interface BrowseFrameLayout.OnFocusSearchListener {
+ method public abstract android.view.View onFocusSearch(android.view.View, int);
+ }
+
+ public final class ClassPresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
+ ctor public ClassPresenterSelector();
+ method public android.support.v17.leanback.widget.ClassPresenterSelector addClassPresenter(java.lang.Class<?>, android.support.v17.leanback.widget.Presenter);
+ method public android.support.v17.leanback.widget.ClassPresenterSelector addClassPresenterSelector(java.lang.Class<?>, android.support.v17.leanback.widget.PresenterSelector);
+ method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
+ }
+
+ public class ControlButtonPresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
+ ctor public ControlButtonPresenterSelector();
+ method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
+ method public android.support.v17.leanback.widget.Presenter getPrimaryPresenter();
+ method public android.support.v17.leanback.widget.Presenter getSecondaryPresenter();
+ }
+
+ public class CursorObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
+ ctor public CursorObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
+ ctor public CursorObjectAdapter(android.support.v17.leanback.widget.Presenter);
+ ctor public CursorObjectAdapter();
+ method public void changeCursor(android.database.Cursor);
+ method public void close();
+ method public java.lang.Object get(int);
+ method public final android.database.Cursor getCursor();
+ method public final android.support.v17.leanback.database.CursorMapper getMapper();
+ method protected final void invalidateCache(int);
+ method protected final void invalidateCache(int, int);
+ method public boolean isClosed();
+ method protected void onCursorChanged();
+ method protected void onMapperChanged();
+ method public final void setMapper(android.support.v17.leanback.database.CursorMapper);
+ method public int size();
+ method public android.database.Cursor swapCursor(android.database.Cursor);
+ }
+
+ public class DetailsOverviewLogoPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public DetailsOverviewLogoPresenter();
+ method public boolean isBoundToImage(android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder, android.support.v17.leanback.widget.DetailsOverviewRow);
+ method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public android.view.View onCreateView(android.view.ViewGroup);
+ method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void setContext(android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
+ }
+
+ public static class DetailsOverviewLogoPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
+ ctor public DetailsOverviewLogoPresenter.ViewHolder(android.view.View);
+ method public android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter getParentPresenter();
+ method public android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder getParentViewHolder();
+ method public boolean isSizeFromDrawableIntrinsic();
+ method public void setSizeFromDrawableIntrinsic(boolean);
+ field protected android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter mParentPresenter;
+ field protected android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder mParentViewHolder;
+ }
+
+ public class DetailsOverviewRow extends android.support.v17.leanback.widget.Row {
+ ctor public DetailsOverviewRow(java.lang.Object);
+ method public final deprecated void addAction(android.support.v17.leanback.widget.Action);
+ method public final deprecated void addAction(int, android.support.v17.leanback.widget.Action);
+ method public android.support.v17.leanback.widget.Action getActionForKeyCode(int);
+ method public final deprecated java.util.List<android.support.v17.leanback.widget.Action> getActions();
+ method public final android.support.v17.leanback.widget.ObjectAdapter getActionsAdapter();
+ method public final android.graphics.drawable.Drawable getImageDrawable();
+ method public final java.lang.Object getItem();
+ method public boolean isImageScaleUpAllowed();
+ method public final deprecated boolean removeAction(android.support.v17.leanback.widget.Action);
+ method public final void setActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public final void setImageBitmap(android.content.Context, android.graphics.Bitmap);
+ method public final void setImageDrawable(android.graphics.drawable.Drawable);
+ method public void setImageScaleUpAllowed(boolean);
+ method public final void setItem(java.lang.Object);
+ }
+
+ public static class DetailsOverviewRow.Listener {
+ ctor public DetailsOverviewRow.Listener();
+ method public void onActionsAdapterChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
+ method public void onImageDrawableChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
+ method public void onItemChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
+ }
+
+ public deprecated class DetailsOverviewRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public DetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter);
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method public int getBackgroundColor();
+ method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
+ method public boolean isStyleLarge();
+ method public final boolean isUsingDefaultSelectEffect();
+ method public void setBackgroundColor(int);
+ method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
+ method public final void setSharedElementEnterTransition(android.app.Activity, java.lang.String, long);
+ method public final void setSharedElementEnterTransition(android.app.Activity, java.lang.String);
+ method public void setStyleLarge(boolean);
+ }
+
+ public final class DetailsOverviewRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public DetailsOverviewRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter);
+ field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDetailsDescriptionViewHolder;
+ }
+
+ public class DetailsParallax extends android.support.v17.leanback.widget.RecyclerViewParallax {
+ ctor public DetailsParallax();
+ method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowBottom();
+ method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowTop();
+ }
+
+ public abstract class DiffCallback<Value> {
+ ctor public DiffCallback();
+ method public abstract boolean areContentsTheSame(Value, Value);
+ method public abstract boolean areItemsTheSame(Value, Value);
+ method public java.lang.Object getChangePayload(Value, Value);
+ }
+
+ public class DividerPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public DividerPresenter();
+ method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ }
+
+ public class DividerRow extends android.support.v17.leanback.widget.Row {
+ ctor public DividerRow();
+ method public final boolean isRenderedAsRowView();
+ }
+
+ public abstract interface FacetProvider {
+ method public abstract java.lang.Object getFacet(java.lang.Class<?>);
+ }
+
+ public abstract interface FacetProviderAdapter {
+ method public abstract android.support.v17.leanback.widget.FacetProvider getFacetProvider(int);
+ }
+
+ public abstract interface FocusHighlight {
+ field public static final int ZOOM_FACTOR_LARGE = 3; // 0x3
+ field public static final int ZOOM_FACTOR_MEDIUM = 2; // 0x2
+ field public static final int ZOOM_FACTOR_NONE = 0; // 0x0
+ field public static final int ZOOM_FACTOR_SMALL = 1; // 0x1
+ field public static final int ZOOM_FACTOR_XSMALL = 4; // 0x4
+ }
+
+ public class FocusHighlightHelper {
+ ctor public FocusHighlightHelper();
+ method public static void setupBrowseItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter, int, boolean);
+ method public static deprecated void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView);
+ method public static deprecated void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView, boolean);
+ method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter);
+ method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter, boolean);
+ }
+
+ public abstract interface FragmentAnimationProvider {
+ method public abstract void onImeAppearing(java.util.List<android.animation.Animator>);
+ method public abstract void onImeDisappearing(java.util.List<android.animation.Animator>);
+ }
+
+ public class FullWidthDetailsOverviewRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public FullWidthDetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter);
+ ctor public FullWidthDetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter, android.support.v17.leanback.widget.DetailsOverviewLogoPresenter);
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method public final int getActionsBackgroundColor();
+ method public final int getAlignmentMode();
+ method public final int getBackgroundColor();
+ method public final int getInitialState();
+ method protected int getLayoutResourceId();
+ method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
+ method public final boolean isParticipatingEntranceTransition();
+ method public final boolean isUsingDefaultSelectEffect();
+ method public final void notifyOnBindLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder);
+ method protected void onLayoutLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, boolean);
+ method protected void onLayoutOverviewFrame(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, boolean);
+ method protected void onStateChanged(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int);
+ method public final void setActionsBackgroundColor(int);
+ method public final void setAlignmentMode(int);
+ method public final void setBackgroundColor(int);
+ method public final void setInitialState(int);
+ method public final void setListener(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.Listener);
+ method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
+ method public final void setParticipatingEntranceTransition(boolean);
+ method public final void setState(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int);
+ field public static final int ALIGN_MODE_MIDDLE = 1; // 0x1
+ field public static final int ALIGN_MODE_START = 0; // 0x0
+ field public static final int STATE_FULL = 1; // 0x1
+ field public static final int STATE_HALF = 0; // 0x0
+ field public static final int STATE_SMALL = 2; // 0x2
+ field protected int mInitialState;
+ }
+
+ public static abstract class FullWidthDetailsOverviewRowPresenter.Listener {
+ ctor public FullWidthDetailsOverviewRowPresenter.Listener();
+ method public void onBindLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder);
+ }
+
+ public class FullWidthDetailsOverviewRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public FullWidthDetailsOverviewRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter, android.support.v17.leanback.widget.DetailsOverviewLogoPresenter);
+ method protected android.support.v17.leanback.widget.DetailsOverviewRow.Listener createRowListener();
+ method public final android.view.ViewGroup getActionsRow();
+ method public final android.view.ViewGroup getDetailsDescriptionFrame();
+ method public final android.support.v17.leanback.widget.Presenter.ViewHolder getDetailsDescriptionViewHolder();
+ method public final android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder getLogoViewHolder();
+ method public final android.view.ViewGroup getOverviewView();
+ method public final int getState();
+ field protected final android.support.v17.leanback.widget.DetailsOverviewRow.Listener mRowListener;
+ }
+
+ public class FullWidthDetailsOverviewRowPresenter.ViewHolder.DetailsOverviewRowListener extends android.support.v17.leanback.widget.DetailsOverviewRow.Listener {
+ ctor public FullWidthDetailsOverviewRowPresenter.ViewHolder.DetailsOverviewRowListener();
+ }
+
+ public class FullWidthDetailsOverviewSharedElementHelper extends android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.Listener {
+ ctor public FullWidthDetailsOverviewSharedElementHelper();
+ method public boolean getAutoStartSharedElementTransition();
+ method public void setAutoStartSharedElementTransition(boolean);
+ method public void setSharedElementEnterTransition(android.app.Activity, java.lang.String);
+ method public void setSharedElementEnterTransition(android.app.Activity, java.lang.String, long);
+ method public void startPostponedEnterTransition();
+ }
+
+ public class GuidanceStylist implements android.support.v17.leanback.widget.FragmentAnimationProvider {
+ ctor public GuidanceStylist();
+ method public android.widget.TextView getBreadcrumbView();
+ method public android.widget.TextView getDescriptionView();
+ method public android.widget.ImageView getIconView();
+ method public android.widget.TextView getTitleView();
+ method public android.view.View onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.support.v17.leanback.widget.GuidanceStylist.Guidance);
+ method public void onDestroyView();
+ method public void onImeAppearing(java.util.List<android.animation.Animator>);
+ method public void onImeDisappearing(java.util.List<android.animation.Animator>);
+ method public int onProvideLayoutId();
+ }
+
+ public static class GuidanceStylist.Guidance {
+ ctor public GuidanceStylist.Guidance(java.lang.String, java.lang.String, java.lang.String, android.graphics.drawable.Drawable);
+ method public java.lang.String getBreadcrumb();
+ method public java.lang.String getDescription();
+ method public android.graphics.drawable.Drawable getIconDrawable();
+ method public java.lang.String getTitle();
+ }
+
+ public class GuidedAction extends android.support.v17.leanback.widget.Action {
+ ctor protected GuidedAction();
+ method public int getCheckSetId();
+ method public java.lang.CharSequence getDescription();
+ method public int getDescriptionEditInputType();
+ method public int getDescriptionInputType();
+ method public java.lang.CharSequence getEditDescription();
+ method public int getEditInputType();
+ method public java.lang.CharSequence getEditTitle();
+ method public int getInputType();
+ method public android.content.Intent getIntent();
+ method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getSubActions();
+ method public java.lang.CharSequence getTitle();
+ method public boolean hasEditableActivatorView();
+ method public boolean hasMultilineDescription();
+ method public boolean hasNext();
+ method public boolean hasSubActions();
+ method public boolean hasTextEditable();
+ method public boolean infoOnly();
+ method public final boolean isAutoSaveRestoreEnabled();
+ method public boolean isChecked();
+ method public boolean isDescriptionEditable();
+ method public boolean isEditTitleUsed();
+ method public boolean isEditable();
+ method public boolean isEnabled();
+ method public boolean isFocusable();
+ method public void onRestoreInstanceState(android.os.Bundle, java.lang.String);
+ method public void onSaveInstanceState(android.os.Bundle, java.lang.String);
+ method public void setChecked(boolean);
+ method public void setDescription(java.lang.CharSequence);
+ method public void setEditDescription(java.lang.CharSequence);
+ method public void setEditTitle(java.lang.CharSequence);
+ method public void setEnabled(boolean);
+ method public void setFocusable(boolean);
+ method public void setIntent(android.content.Intent);
+ method public void setSubActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public void setTitle(java.lang.CharSequence);
+ field public static final long ACTION_ID_CANCEL = -5L; // 0xfffffffffffffffbL
+ field public static final long ACTION_ID_CONTINUE = -7L; // 0xfffffffffffffff9L
+ field public static final long ACTION_ID_CURRENT = -3L; // 0xfffffffffffffffdL
+ field public static final long ACTION_ID_FINISH = -6L; // 0xfffffffffffffffaL
+ field public static final long ACTION_ID_NEXT = -2L; // 0xfffffffffffffffeL
+ field public static final long ACTION_ID_NO = -9L; // 0xfffffffffffffff7L
+ field public static final long ACTION_ID_OK = -4L; // 0xfffffffffffffffcL
+ field public static final long ACTION_ID_YES = -8L; // 0xfffffffffffffff8L
+ field public static final int CHECKBOX_CHECK_SET_ID = -1; // 0xffffffff
+ field public static final int DEFAULT_CHECK_SET_ID = 1; // 0x1
+ field public static final int NO_CHECK_SET = 0; // 0x0
+ }
+
+ public static class GuidedAction.Builder extends android.support.v17.leanback.widget.GuidedAction.BuilderBase {
+ ctor public deprecated GuidedAction.Builder();
+ ctor public GuidedAction.Builder(android.content.Context);
+ method public android.support.v17.leanback.widget.GuidedAction build();
+ }
+
+ public static abstract class GuidedAction.BuilderBase<B extends android.support.v17.leanback.widget.GuidedAction.BuilderBase> {
+ ctor public GuidedAction.BuilderBase(android.content.Context);
+ method protected final void applyValues(android.support.v17.leanback.widget.GuidedAction);
+ method public B autoSaveRestoreEnabled(boolean);
+ method public B checkSetId(int);
+ method public B checked(boolean);
+ method public B clickAction(long);
+ method public B description(java.lang.CharSequence);
+ method public B description(int);
+ method public B descriptionEditInputType(int);
+ method public B descriptionEditable(boolean);
+ method public B descriptionInputType(int);
+ method public B editDescription(java.lang.CharSequence);
+ method public B editDescription(int);
+ method public B editInputType(int);
+ method public B editTitle(java.lang.CharSequence);
+ method public B editTitle(int);
+ method public B editable(boolean);
+ method public B enabled(boolean);
+ method public B focusable(boolean);
+ method public android.content.Context getContext();
+ method public B hasEditableActivatorView(boolean);
+ method public B hasNext(boolean);
+ method public B icon(android.graphics.drawable.Drawable);
+ method public B icon(int);
+ method public deprecated B iconResourceId(int, android.content.Context);
+ method public B id(long);
+ method public B infoOnly(boolean);
+ method public B inputType(int);
+ method public B intent(android.content.Intent);
+ method public B multilineDescription(boolean);
+ method public B subActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public B title(java.lang.CharSequence);
+ method public B title(int);
+ }
+
+ public class GuidedActionDiffCallback extends android.support.v17.leanback.widget.DiffCallback {
+ ctor public GuidedActionDiffCallback();
+ method public boolean areContentsTheSame(android.support.v17.leanback.widget.GuidedAction, android.support.v17.leanback.widget.GuidedAction);
+ method public boolean areItemsTheSame(android.support.v17.leanback.widget.GuidedAction, android.support.v17.leanback.widget.GuidedAction);
+ method public static final android.support.v17.leanback.widget.GuidedActionDiffCallback getInstance();
+ }
+
+ public class GuidedActionEditText extends android.widget.EditText implements android.support.v17.leanback.widget.ImeKeyMonitor {
+ ctor public GuidedActionEditText(android.content.Context);
+ ctor public GuidedActionEditText(android.content.Context, android.util.AttributeSet);
+ ctor public GuidedActionEditText(android.content.Context, android.util.AttributeSet, int);
+ method public void setImeKeyListener(android.support.v17.leanback.widget.ImeKeyMonitor.ImeKeyListener);
+ }
+
+ public class GuidedActionsStylist implements android.support.v17.leanback.widget.FragmentAnimationProvider {
+ ctor public GuidedActionsStylist();
+ method public void collapseAction(boolean);
+ method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
+ method public android.support.v17.leanback.widget.VerticalGridView getActionsGridView();
+ method public android.support.v17.leanback.widget.GuidedAction getExpandedAction();
+ method public int getItemViewType(android.support.v17.leanback.widget.GuidedAction);
+ method public android.support.v17.leanback.widget.VerticalGridView getSubActionsGridView();
+ method public final boolean isBackKeyToCollapseActivatorView();
+ method public final boolean isBackKeyToCollapseSubActions();
+ method public boolean isButtonActions();
+ method public boolean isExpandTransitionSupported();
+ method public boolean isExpanded();
+ method public boolean isInExpandTransition();
+ method public boolean isSubActionsExpanded();
+ method public void onAnimateItemChecked(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
+ method public void onAnimateItemFocused(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
+ method public void onAnimateItemPressed(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
+ method public void onAnimateItemPressedCancelled(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ method public void onBindActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public void onBindCheckMarkView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public void onBindChevronView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public void onBindViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public android.view.View onCreateView(android.view.LayoutInflater, android.view.ViewGroup);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder onCreateViewHolder(android.view.ViewGroup, int);
+ method public void onDestroyView();
+ method protected deprecated void onEditingModeChange(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction, boolean);
+ method protected void onEditingModeChange(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean, boolean);
+ method public void onImeAppearing(java.util.List<android.animation.Animator>);
+ method public void onImeDisappearing(java.util.List<android.animation.Animator>);
+ method public int onProvideItemLayoutId();
+ method public int onProvideItemLayoutId(int);
+ method public int onProvideLayoutId();
+ method public boolean onUpdateActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public void onUpdateExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
+ method public void setAsButtonActions();
+ method public final void setBackKeyToCollapseActivatorView(boolean);
+ method public final void setBackKeyToCollapseSubActions(boolean);
+ method public deprecated void setEditingMode(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction, boolean);
+ method public deprecated void setExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ method protected void setupImeOptions(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
+ method public deprecated void startExpandedTransition(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ field public static final int VIEW_TYPE_DATE_PICKER = 1; // 0x1
+ field public static final int VIEW_TYPE_DEFAULT = 0; // 0x0
+ }
+
+ public static class GuidedActionsStylist.ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
+ ctor public GuidedActionsStylist.ViewHolder(android.view.View);
+ ctor public GuidedActionsStylist.ViewHolder(android.view.View, boolean);
+ method public android.support.v17.leanback.widget.GuidedAction getAction();
+ method public android.widget.ImageView getCheckmarkView();
+ method public android.widget.ImageView getChevronView();
+ method public android.view.View getContentView();
+ method public android.widget.TextView getDescriptionView();
+ method public android.widget.EditText getEditableDescriptionView();
+ method public android.widget.EditText getEditableTitleView();
+ method public android.view.View getEditingView();
+ method public java.lang.Object getFacet(java.lang.Class<?>);
+ method public android.widget.ImageView getIconView();
+ method public android.widget.TextView getTitleView();
+ method public boolean isInEditing();
+ method public boolean isInEditingActivatorView();
+ method public boolean isInEditingDescription();
+ method public boolean isInEditingText();
+ method public boolean isInEditingTitle();
+ method public boolean isSubAction();
+ }
+
+ public class GuidedDatePickerAction extends android.support.v17.leanback.widget.GuidedAction {
+ ctor public GuidedDatePickerAction();
+ method public long getDate();
+ method public java.lang.String getDatePickerFormat();
+ method public long getMaxDate();
+ method public long getMinDate();
+ method public void setDate(long);
+ }
+
+ public static final class GuidedDatePickerAction.Builder extends android.support.v17.leanback.widget.GuidedDatePickerAction.BuilderBase {
+ ctor public GuidedDatePickerAction.Builder(android.content.Context);
+ method public android.support.v17.leanback.widget.GuidedDatePickerAction build();
+ }
+
+ public static abstract class GuidedDatePickerAction.BuilderBase<B extends android.support.v17.leanback.widget.GuidedDatePickerAction.BuilderBase> extends android.support.v17.leanback.widget.GuidedAction.BuilderBase {
+ ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
+ method protected final void applyDatePickerValues(android.support.v17.leanback.widget.GuidedDatePickerAction);
+ method public B date(long);
+ method public B datePickerFormat(java.lang.String);
+ method public B maxDate(long);
+ method public B minDate(long);
+ }
+
+ public class HeaderItem {
+ ctor public HeaderItem(long, java.lang.String);
+ ctor public HeaderItem(java.lang.String);
+ method public java.lang.CharSequence getContentDescription();
+ method public java.lang.CharSequence getDescription();
+ method public final long getId();
+ method public final java.lang.String getName();
+ method public void setContentDescription(java.lang.CharSequence);
+ method public void setDescription(java.lang.CharSequence);
+ }
+
+ public class HorizontalGridView extends android.support.v17.leanback.widget.BaseGridView {
+ ctor public HorizontalGridView(android.content.Context);
+ ctor public HorizontalGridView(android.content.Context, android.util.AttributeSet);
+ ctor public HorizontalGridView(android.content.Context, android.util.AttributeSet, int);
+ method public final boolean getFadingLeftEdge();
+ method public final int getFadingLeftEdgeLength();
+ method public final int getFadingLeftEdgeOffset();
+ method public final boolean getFadingRightEdge();
+ method public final int getFadingRightEdgeLength();
+ method public final int getFadingRightEdgeOffset();
+ method protected void initAttributes(android.content.Context, android.util.AttributeSet);
+ method public final void setFadingLeftEdge(boolean);
+ method public final void setFadingLeftEdgeLength(int);
+ method public final void setFadingLeftEdgeOffset(int);
+ method public final void setFadingRightEdge(boolean);
+ method public final void setFadingRightEdgeLength(int);
+ method public final void setFadingRightEdgeOffset(int);
+ method public void setNumRows(int);
+ method public void setRowHeight(int);
+ }
+
+ public final class HorizontalHoverCardSwitcher extends android.support.v17.leanback.widget.PresenterSwitcher {
+ ctor public HorizontalHoverCardSwitcher();
+ method protected void insertView(android.view.View);
+ method public void select(android.support.v17.leanback.widget.HorizontalGridView, android.view.View, java.lang.Object);
+ }
+
+ public class ImageCardView extends android.support.v17.leanback.widget.BaseCardView {
+ ctor public deprecated ImageCardView(android.content.Context, int);
+ ctor public ImageCardView(android.content.Context, android.util.AttributeSet, int);
+ ctor public ImageCardView(android.content.Context);
+ ctor public ImageCardView(android.content.Context, android.util.AttributeSet);
+ method public android.graphics.drawable.Drawable getBadgeImage();
+ method public java.lang.CharSequence getContentText();
+ method public android.graphics.drawable.Drawable getInfoAreaBackground();
+ method public android.graphics.drawable.Drawable getMainImage();
+ method public final android.widget.ImageView getMainImageView();
+ method public java.lang.CharSequence getTitleText();
+ method public void setBadgeImage(android.graphics.drawable.Drawable);
+ method public void setContentText(java.lang.CharSequence);
+ method public void setInfoAreaBackground(android.graphics.drawable.Drawable);
+ method public void setInfoAreaBackgroundColor(int);
+ method public void setMainImage(android.graphics.drawable.Drawable);
+ method public void setMainImage(android.graphics.drawable.Drawable, boolean);
+ method public void setMainImageAdjustViewBounds(boolean);
+ method public void setMainImageDimensions(int, int);
+ method public void setMainImageScaleType(android.widget.ImageView.ScaleType);
+ method public void setTitleText(java.lang.CharSequence);
+ field public static final int CARD_TYPE_FLAG_CONTENT = 2; // 0x2
+ field public static final int CARD_TYPE_FLAG_ICON_LEFT = 8; // 0x8
+ field public static final int CARD_TYPE_FLAG_ICON_RIGHT = 4; // 0x4
+ field public static final int CARD_TYPE_FLAG_IMAGE_ONLY = 0; // 0x0
+ field public static final int CARD_TYPE_FLAG_TITLE = 1; // 0x1
+ }
+
+ public abstract interface ImeKeyMonitor {
+ method public abstract void setImeKeyListener(android.support.v17.leanback.widget.ImeKeyMonitor.ImeKeyListener);
+ }
+
+ public static abstract interface ImeKeyMonitor.ImeKeyListener {
+ method public abstract boolean onKeyPreIme(android.widget.EditText, int, android.view.KeyEvent);
+ }
+
+ public final class ItemAlignmentFacet {
+ ctor public ItemAlignmentFacet();
+ method public android.support.v17.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef[] getAlignmentDefs();
+ method public boolean isMultiAlignment();
+ method public void setAlignmentDefs(android.support.v17.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef[]);
+ field public static final float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
+ }
+
+ public static class ItemAlignmentFacet.ItemAlignmentDef {
+ ctor public ItemAlignmentFacet.ItemAlignmentDef();
+ method public final int getItemAlignmentFocusViewId();
+ method public final int getItemAlignmentOffset();
+ method public final float getItemAlignmentOffsetPercent();
+ method public final int getItemAlignmentViewId();
+ method public boolean isAlignedToTextViewBaseLine();
+ method public final boolean isItemAlignmentOffsetWithPadding();
+ method public final void setAlignedToTextViewBaseline(boolean);
+ method public final void setItemAlignmentFocusViewId(int);
+ method public final void setItemAlignmentOffset(int);
+ method public final void setItemAlignmentOffsetPercent(float);
+ method public final void setItemAlignmentOffsetWithPadding(boolean);
+ method public final void setItemAlignmentViewId(int);
+ }
+
+ public class ItemBridgeAdapter extends android.support.v7.widget.RecyclerView.Adapter implements android.support.v17.leanback.widget.FacetProviderAdapter {
+ ctor public ItemBridgeAdapter(android.support.v17.leanback.widget.ObjectAdapter, android.support.v17.leanback.widget.PresenterSelector);
+ ctor public ItemBridgeAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ ctor public ItemBridgeAdapter();
+ method public void clear();
+ method public android.support.v17.leanback.widget.FacetProvider getFacetProvider(int);
+ method public int getItemCount();
+ method public java.util.ArrayList<android.support.v17.leanback.widget.Presenter> getPresenterMapper();
+ method public android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper getWrapper();
+ method protected void onAddPresenter(android.support.v17.leanback.widget.Presenter, int);
+ method protected void onAttachedToWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method protected void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public final void onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int);
+ method public final void onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int, java.util.List);
+ method protected void onCreate(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public final android.support.v7.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int);
+ method protected void onDetachedFromWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method protected void onUnbind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public final void onViewAttachedToWindow(android.support.v7.widget.RecyclerView.ViewHolder);
+ method public final void onViewDetachedFromWindow(android.support.v7.widget.RecyclerView.ViewHolder);
+ method public final void onViewRecycled(android.support.v7.widget.RecyclerView.ViewHolder);
+ method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public void setAdapterListener(android.support.v17.leanback.widget.ItemBridgeAdapter.AdapterListener);
+ method public void setPresenter(android.support.v17.leanback.widget.PresenterSelector);
+ method public void setPresenterMapper(java.util.ArrayList<android.support.v17.leanback.widget.Presenter>);
+ method public void setWrapper(android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper);
+ }
+
+ public static class ItemBridgeAdapter.AdapterListener {
+ ctor public ItemBridgeAdapter.AdapterListener();
+ method public void onAddPresenter(android.support.v17.leanback.widget.Presenter, int);
+ method public void onAttachedToWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder, java.util.List);
+ method public void onCreate(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public void onDetachedFromWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ method public void onUnbind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
+ }
+
+ public class ItemBridgeAdapter.ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
+ method public final java.lang.Object getExtraObject();
+ method public java.lang.Object getFacet(java.lang.Class<?>);
+ method public final java.lang.Object getItem();
+ method public final android.support.v17.leanback.widget.Presenter getPresenter();
+ method public final android.support.v17.leanback.widget.Presenter.ViewHolder getViewHolder();
+ method public void setExtraObject(java.lang.Object);
+ }
+
+ public static abstract class ItemBridgeAdapter.Wrapper {
+ ctor public ItemBridgeAdapter.Wrapper();
+ method public abstract android.view.View createWrapper(android.view.View);
+ method public abstract void wrap(android.view.View, android.view.View);
+ }
+
+ public class ItemBridgeAdapterShadowOverlayWrapper extends android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper {
+ ctor public ItemBridgeAdapterShadowOverlayWrapper(android.support.v17.leanback.widget.ShadowOverlayHelper);
+ method public android.view.View createWrapper(android.view.View);
+ method public void wrap(android.view.View, android.view.View);
+ }
+
+ public class ListRow extends android.support.v17.leanback.widget.Row {
+ ctor public ListRow(android.support.v17.leanback.widget.HeaderItem, android.support.v17.leanback.widget.ObjectAdapter);
+ ctor public ListRow(long, android.support.v17.leanback.widget.HeaderItem, android.support.v17.leanback.widget.ObjectAdapter);
+ ctor public ListRow(android.support.v17.leanback.widget.ObjectAdapter);
+ method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
+ method public java.lang.CharSequence getContentDescription();
+ method public void setContentDescription(java.lang.CharSequence);
+ }
+
+ public final class ListRowHoverCardView extends android.widget.LinearLayout {
+ ctor public ListRowHoverCardView(android.content.Context);
+ ctor public ListRowHoverCardView(android.content.Context, android.util.AttributeSet);
+ ctor public ListRowHoverCardView(android.content.Context, android.util.AttributeSet, int);
+ method public final java.lang.CharSequence getDescription();
+ method public final java.lang.CharSequence getTitle();
+ method public final void setDescription(java.lang.CharSequence);
+ method public final void setTitle(java.lang.CharSequence);
+ }
+
+ public class ListRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public ListRowPresenter();
+ ctor public ListRowPresenter(int);
+ ctor public ListRowPresenter(int, boolean);
+ method protected void applySelectLevelToChild(android.support.v17.leanback.widget.ListRowPresenter.ViewHolder, android.view.View);
+ method public final boolean areChildRoundedCornersEnabled();
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method protected android.support.v17.leanback.widget.ShadowOverlayHelper.Options createShadowOverlayOptions();
+ method public final void enableChildRoundedCorners(boolean);
+ method public int getExpandedRowHeight();
+ method public final int getFocusZoomFactor();
+ method public final android.support.v17.leanback.widget.PresenterSelector getHoverCardPresenterSelector();
+ method public int getRecycledPoolSize(android.support.v17.leanback.widget.Presenter);
+ method public int getRowHeight();
+ method public final boolean getShadowEnabled();
+ method public final deprecated int getZoomFactor();
+ method public final boolean isFocusDimmerUsed();
+ method public final boolean isKeepChildForeground();
+ method public boolean isUsingDefaultListSelectEffect();
+ method public final boolean isUsingDefaultSelectEffect();
+ method public boolean isUsingDefaultShadow();
+ method public boolean isUsingOutlineClipping(android.content.Context);
+ method public boolean isUsingZOrder(android.content.Context);
+ method public void setExpandedRowHeight(int);
+ method public final void setHoverCardPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public final void setKeepChildForeground(boolean);
+ method public void setNumRows(int);
+ method public void setRecycledPoolSize(android.support.v17.leanback.widget.Presenter, int);
+ method public void setRowHeight(int);
+ method public final void setShadowEnabled(boolean);
+ }
+
+ public static class ListRowPresenter.SelectItemViewHolderTask extends android.support.v17.leanback.widget.Presenter.ViewHolderTask {
+ ctor public ListRowPresenter.SelectItemViewHolderTask(int);
+ method public int getItemPosition();
+ method public android.support.v17.leanback.widget.Presenter.ViewHolderTask getItemTask();
+ method public boolean isSmoothScroll();
+ method public void setItemPosition(int);
+ method public void setItemTask(android.support.v17.leanback.widget.Presenter.ViewHolderTask);
+ method public void setSmoothScroll(boolean);
+ }
+
+ public static class ListRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public ListRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.HorizontalGridView, android.support.v17.leanback.widget.ListRowPresenter);
+ method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
+ method public final android.support.v17.leanback.widget.HorizontalGridView getGridView();
+ method public android.support.v17.leanback.widget.Presenter.ViewHolder getItemViewHolder(int);
+ method public final android.support.v17.leanback.widget.ListRowPresenter getListRowPresenter();
+ method public int getSelectedPosition();
+ }
+
+ public final class ListRowView extends android.widget.LinearLayout {
+ ctor public ListRowView(android.content.Context);
+ ctor public ListRowView(android.content.Context, android.util.AttributeSet);
+ ctor public ListRowView(android.content.Context, android.util.AttributeSet, int);
+ method public android.support.v17.leanback.widget.HorizontalGridView getGridView();
+ }
+
+ public abstract interface MultiActionsProvider {
+ method public abstract android.support.v17.leanback.widget.MultiActionsProvider.MultiAction[] getActions();
+ }
+
+ public static class MultiActionsProvider.MultiAction {
+ ctor public MultiActionsProvider.MultiAction(long);
+ method public android.graphics.drawable.Drawable getCurrentDrawable();
+ method public android.graphics.drawable.Drawable[] getDrawables();
+ method public long getId();
+ method public int getIndex();
+ method public void incrementIndex();
+ method public void setDrawables(android.graphics.drawable.Drawable[]);
+ method public void setIndex(int);
+ }
+
+ public abstract class ObjectAdapter {
+ ctor public ObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
+ ctor public ObjectAdapter(android.support.v17.leanback.widget.Presenter);
+ ctor public ObjectAdapter();
+ method public abstract java.lang.Object get(int);
+ method public long getId(int);
+ method public final android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
+ method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
+ method public final boolean hasStableIds();
+ method public boolean isImmediateNotifySupported();
+ method protected final void notifyChanged();
+ method protected final void notifyItemMoved(int, int);
+ method public final void notifyItemRangeChanged(int, int);
+ method public final void notifyItemRangeChanged(int, int, java.lang.Object);
+ method protected final void notifyItemRangeInserted(int, int);
+ method protected final void notifyItemRangeRemoved(int, int);
+ method protected void onHasStableIdsChanged();
+ method protected void onPresenterSelectorChanged();
+ method public final void registerObserver(android.support.v17.leanback.widget.ObjectAdapter.DataObserver);
+ method public final void setHasStableIds(boolean);
+ method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
+ method public abstract int size();
+ method public final void unregisterAllObservers();
+ method public final void unregisterObserver(android.support.v17.leanback.widget.ObjectAdapter.DataObserver);
+ field public static final int NO_ID = -1; // 0xffffffff
+ }
+
+ public static abstract class ObjectAdapter.DataObserver {
+ ctor public ObjectAdapter.DataObserver();
+ method public void onChanged();
+ method public void onItemMoved(int, int);
+ method public void onItemRangeChanged(int, int);
+ method public void onItemRangeChanged(int, int, java.lang.Object);
+ method public void onItemRangeInserted(int, int);
+ method public void onItemRangeRemoved(int, int);
+ }
+
+ public abstract interface OnActionClickedListener {
+ method public abstract void onActionClicked(android.support.v17.leanback.widget.Action);
+ }
+
+ public abstract interface OnChildLaidOutListener {
+ method public abstract void onChildLaidOut(android.view.ViewGroup, android.view.View, int, long);
+ }
+
+ public abstract deprecated interface OnChildSelectedListener {
+ method public abstract void onChildSelected(android.view.ViewGroup, android.view.View, int, long);
+ }
+
+ public abstract class OnChildViewHolderSelectedListener {
+ ctor public OnChildViewHolderSelectedListener();
+ method public void onChildViewHolderSelected(android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.ViewHolder, int, int);
+ method public void onChildViewHolderSelectedAndPositioned(android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.ViewHolder, int, int);
+ }
+
+ public abstract interface OnItemViewClickedListener implements android.support.v17.leanback.widget.BaseOnItemViewClickedListener {
+ }
+
+ public abstract interface OnItemViewSelectedListener implements android.support.v17.leanback.widget.BaseOnItemViewSelectedListener {
+ }
+
+ public class PageRow extends android.support.v17.leanback.widget.Row {
+ ctor public PageRow(android.support.v17.leanback.widget.HeaderItem);
+ method public final boolean isRenderedAsRowView();
+ }
+
+ public abstract class Parallax<PropertyT extends android.util.Property> {
+ ctor public Parallax();
+ method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.PropertyMarkerValue...);
+ method public final PropertyT addProperty(java.lang.String);
+ method public abstract PropertyT createProperty(java.lang.String, int);
+ method public java.util.List<android.support.v17.leanback.widget.ParallaxEffect> getEffects();
+ method public abstract float getMaxValue();
+ method public final java.util.List<PropertyT> getProperties();
+ method public void removeAllEffects();
+ method public void removeEffect(android.support.v17.leanback.widget.ParallaxEffect);
+ method public void updateValues();
+ }
+
+ public static class Parallax.FloatProperty extends android.util.Property {
+ ctor public Parallax.FloatProperty(java.lang.String, int);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue at(float, float);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atAbsolute(float);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atFraction(float);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMax();
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMin();
+ method public final java.lang.Float get(android.support.v17.leanback.widget.Parallax);
+ method public final int getIndex();
+ method public final float getValue(android.support.v17.leanback.widget.Parallax);
+ method public final void set(android.support.v17.leanback.widget.Parallax, java.lang.Float);
+ method public final void setValue(android.support.v17.leanback.widget.Parallax, float);
+ field public static final float UNKNOWN_AFTER = 3.4028235E38f;
+ field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
+ }
+
+ public static class Parallax.IntProperty extends android.util.Property {
+ ctor public Parallax.IntProperty(java.lang.String, int);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue at(int, float);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atAbsolute(int);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atFraction(float);
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMax();
+ method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMin();
+ method public final java.lang.Integer get(android.support.v17.leanback.widget.Parallax);
+ method public final int getIndex();
+ method public final int getValue(android.support.v17.leanback.widget.Parallax);
+ method public final void set(android.support.v17.leanback.widget.Parallax, java.lang.Integer);
+ method public final void setValue(android.support.v17.leanback.widget.Parallax, int);
+ field public static final int UNKNOWN_AFTER = 2147483647; // 0x7fffffff
+ field public static final int UNKNOWN_BEFORE = -2147483648; // 0x80000000
+ }
+
+ public static class Parallax.PropertyMarkerValue<PropertyT> {
+ ctor public Parallax.PropertyMarkerValue(PropertyT);
+ method public PropertyT getProperty();
+ }
+
+ public abstract class ParallaxEffect {
+ method public final void addTarget(android.support.v17.leanback.widget.ParallaxTarget);
+ method public final java.util.List<android.support.v17.leanback.widget.Parallax.PropertyMarkerValue> getPropertyRanges();
+ method public final java.util.List<android.support.v17.leanback.widget.ParallaxTarget> getTargets();
+ method public final void performMapping(android.support.v17.leanback.widget.Parallax);
+ method public final void removeTarget(android.support.v17.leanback.widget.ParallaxTarget);
+ method public final void setPropertyRanges(android.support.v17.leanback.widget.Parallax.PropertyMarkerValue...);
+ method public final android.support.v17.leanback.widget.ParallaxEffect target(android.support.v17.leanback.widget.ParallaxTarget);
+ method public final android.support.v17.leanback.widget.ParallaxEffect target(java.lang.Object, android.animation.PropertyValuesHolder);
+ method public final <T, V extends java.lang.Number> android.support.v17.leanback.widget.ParallaxEffect target(T, android.util.Property<T, V>);
+ }
+
+ public abstract class ParallaxTarget {
+ ctor public ParallaxTarget();
+ method public void directUpdate(java.lang.Number);
+ method public boolean isDirectMapping();
+ method public void update(float);
+ }
+
+ public static final class ParallaxTarget.DirectPropertyTarget<T, V extends java.lang.Number> extends android.support.v17.leanback.widget.ParallaxTarget {
+ ctor public ParallaxTarget.DirectPropertyTarget(java.lang.Object, android.util.Property<T, V>);
+ }
+
+ public static final class ParallaxTarget.PropertyValuesHolderTarget extends android.support.v17.leanback.widget.ParallaxTarget {
+ ctor public ParallaxTarget.PropertyValuesHolderTarget(java.lang.Object, android.animation.PropertyValuesHolder);
+ }
+
+ public class PlaybackControlsRow extends android.support.v17.leanback.widget.Row {
+ ctor public PlaybackControlsRow(java.lang.Object);
+ ctor public PlaybackControlsRow();
+ method public android.support.v17.leanback.widget.Action getActionForKeyCode(int);
+ method public android.support.v17.leanback.widget.Action getActionForKeyCode(android.support.v17.leanback.widget.ObjectAdapter, int);
+ method public long getBufferedPosition();
+ method public deprecated int getBufferedProgress();
+ method public deprecated long getBufferedProgressLong();
+ method public long getCurrentPosition();
+ method public deprecated int getCurrentTime();
+ method public deprecated long getCurrentTimeLong();
+ method public long getDuration();
+ method public final android.graphics.drawable.Drawable getImageDrawable();
+ method public final java.lang.Object getItem();
+ method public final android.support.v17.leanback.widget.ObjectAdapter getPrimaryActionsAdapter();
+ method public final android.support.v17.leanback.widget.ObjectAdapter getSecondaryActionsAdapter();
+ method public deprecated int getTotalTime();
+ method public deprecated long getTotalTimeLong();
+ method public void setBufferedPosition(long);
+ method public deprecated void setBufferedProgress(int);
+ method public deprecated void setBufferedProgressLong(long);
+ method public void setCurrentPosition(long);
+ method public deprecated void setCurrentTime(int);
+ method public deprecated void setCurrentTimeLong(long);
+ method public void setDuration(long);
+ method public final void setImageBitmap(android.content.Context, android.graphics.Bitmap);
+ method public final void setImageDrawable(android.graphics.drawable.Drawable);
+ method public void setOnPlaybackProgressChangedListener(android.support.v17.leanback.widget.PlaybackControlsRow.OnPlaybackProgressCallback);
+ method public final void setPrimaryActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public final void setSecondaryActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
+ method public deprecated void setTotalTime(int);
+ method public deprecated void setTotalTimeLong(long);
+ }
+
+ public static class PlaybackControlsRow.ClosedCaptioningAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.ClosedCaptioningAction(android.content.Context);
+ ctor public PlaybackControlsRow.ClosedCaptioningAction(android.content.Context, int);
+ field public static final int INDEX_OFF = 0; // 0x0
+ field public static final int INDEX_ON = 1; // 0x1
+ field public static deprecated int OFF;
+ field public static deprecated int ON;
+ }
+
+ public static class PlaybackControlsRow.FastForwardAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.FastForwardAction(android.content.Context);
+ ctor public PlaybackControlsRow.FastForwardAction(android.content.Context, int);
+ }
+
+ public static class PlaybackControlsRow.HighQualityAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.HighQualityAction(android.content.Context);
+ ctor public PlaybackControlsRow.HighQualityAction(android.content.Context, int);
+ field public static final int INDEX_OFF = 0; // 0x0
+ field public static final int INDEX_ON = 1; // 0x1
+ field public static deprecated int OFF;
+ field public static deprecated int ON;
+ }
+
+ public static class PlaybackControlsRow.MoreActions extends android.support.v17.leanback.widget.Action {
+ ctor public PlaybackControlsRow.MoreActions(android.content.Context);
+ }
+
+ public static abstract class PlaybackControlsRow.MultiAction extends android.support.v17.leanback.widget.Action {
+ ctor public PlaybackControlsRow.MultiAction(int);
+ method public int getActionCount();
+ method public android.graphics.drawable.Drawable getDrawable(int);
+ method public int getIndex();
+ method public java.lang.String getLabel(int);
+ method public java.lang.String getSecondaryLabel(int);
+ method public void nextIndex();
+ method public void setDrawables(android.graphics.drawable.Drawable[]);
+ method public void setIndex(int);
+ method public void setLabels(java.lang.String[]);
+ method public void setSecondaryLabels(java.lang.String[]);
+ }
+
+ public static class PlaybackControlsRow.OnPlaybackProgressCallback {
+ ctor public PlaybackControlsRow.OnPlaybackProgressCallback();
+ method public void onBufferedPositionChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
+ method public void onCurrentPositionChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
+ method public void onDurationChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
+ }
+
+ public static class PlaybackControlsRow.PictureInPictureAction extends android.support.v17.leanback.widget.Action {
+ ctor public PlaybackControlsRow.PictureInPictureAction(android.content.Context);
+ }
+
+ public static class PlaybackControlsRow.PlayPauseAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.PlayPauseAction(android.content.Context);
+ field public static final int INDEX_PAUSE = 1; // 0x1
+ field public static final int INDEX_PLAY = 0; // 0x0
+ field public static deprecated int PAUSE;
+ field public static deprecated int PLAY;
+ }
+
+ public static class PlaybackControlsRow.RepeatAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.RepeatAction(android.content.Context);
+ ctor public PlaybackControlsRow.RepeatAction(android.content.Context, int);
+ ctor public PlaybackControlsRow.RepeatAction(android.content.Context, int, int);
+ field public static deprecated int ALL;
+ field public static final int INDEX_ALL = 1; // 0x1
+ field public static final int INDEX_NONE = 0; // 0x0
+ field public static final int INDEX_ONE = 2; // 0x2
+ field public static deprecated int NONE;
+ field public static deprecated int ONE;
+ }
+
+ public static class PlaybackControlsRow.RewindAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.RewindAction(android.content.Context);
+ ctor public PlaybackControlsRow.RewindAction(android.content.Context, int);
+ }
+
+ public static class PlaybackControlsRow.ShuffleAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.ShuffleAction(android.content.Context);
+ ctor public PlaybackControlsRow.ShuffleAction(android.content.Context, int);
+ field public static final int INDEX_OFF = 0; // 0x0
+ field public static final int INDEX_ON = 1; // 0x1
+ field public static deprecated int OFF;
+ field public static deprecated int ON;
+ }
+
+ public static class PlaybackControlsRow.SkipNextAction extends android.support.v17.leanback.widget.Action {
+ ctor public PlaybackControlsRow.SkipNextAction(android.content.Context);
+ }
+
+ public static class PlaybackControlsRow.SkipPreviousAction extends android.support.v17.leanback.widget.Action {
+ ctor public PlaybackControlsRow.SkipPreviousAction(android.content.Context);
+ }
+
+ public static abstract class PlaybackControlsRow.ThumbsAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
+ ctor public PlaybackControlsRow.ThumbsAction(int, android.content.Context, int, int);
+ field public static final int INDEX_OUTLINE = 1; // 0x1
+ field public static final int INDEX_SOLID = 0; // 0x0
+ field public static deprecated int OUTLINE;
+ field public static deprecated int SOLID;
+ }
+
+ public static class PlaybackControlsRow.ThumbsDownAction extends android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsAction {
+ ctor public PlaybackControlsRow.ThumbsDownAction(android.content.Context);
+ }
+
+ public static class PlaybackControlsRow.ThumbsUpAction extends android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsAction {
+ ctor public PlaybackControlsRow.ThumbsUpAction(android.content.Context);
+ }
+
+ public class PlaybackControlsRowPresenter extends android.support.v17.leanback.widget.PlaybackRowPresenter {
+ ctor public PlaybackControlsRowPresenter(android.support.v17.leanback.widget.Presenter);
+ ctor public PlaybackControlsRowPresenter();
+ method public boolean areSecondaryActionsHidden();
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method public int getBackgroundColor();
+ method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
+ method public int getProgressColor();
+ method public void setBackgroundColor(int);
+ method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
+ method public void setProgressColor(int);
+ method public void setSecondaryActionsHidden(boolean);
+ method public void showBottomSpace(android.support.v17.leanback.widget.PlaybackControlsRowPresenter.ViewHolder, boolean);
+ method public void showPrimaryActions(android.support.v17.leanback.widget.PlaybackControlsRowPresenter.ViewHolder);
+ }
+
+ public class PlaybackControlsRowPresenter.ViewHolder extends android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder {
+ field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDescriptionViewHolder;
+ }
+
+ public abstract class PlaybackRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
+ ctor public PlaybackRowPresenter();
+ method public void onReappear(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ }
+
+ public static class PlaybackRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
+ ctor public PlaybackRowPresenter.ViewHolder(android.view.View);
+ }
+
+ public class PlaybackSeekDataProvider {
+ ctor public PlaybackSeekDataProvider();
+ method public long[] getSeekPositions();
+ method public void getThumbnail(int, android.support.v17.leanback.widget.PlaybackSeekDataProvider.ResultCallback);
+ method public void reset();
+ }
+
+ public static class PlaybackSeekDataProvider.ResultCallback {
+ ctor public PlaybackSeekDataProvider.ResultCallback();
+ method public void onThumbnailLoaded(android.graphics.Bitmap, int);
+ }
+
+ public abstract interface PlaybackSeekUi {
+ method public abstract void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ }
+
+ public static class PlaybackSeekUi.Client {
+ ctor public PlaybackSeekUi.Client();
+ method public android.support.v17.leanback.widget.PlaybackSeekDataProvider getPlaybackSeekDataProvider();
+ method public boolean isSeekEnabled();
+ method public void onSeekFinished(boolean);
+ method public void onSeekPositionChanged(long);
+ method public void onSeekStarted();
+ }
+
+ public class PlaybackTransportRowPresenter extends android.support.v17.leanback.widget.PlaybackRowPresenter {
+ ctor public PlaybackTransportRowPresenter();
+ method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method public float getDefaultSeekIncrement();
+ method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
+ method public int getProgressColor();
+ method protected void onProgressBarClicked(android.support.v17.leanback.widget.PlaybackTransportRowPresenter.ViewHolder);
+ method public void setDefaultSeekIncrement(float);
+ method public void setDescriptionPresenter(android.support.v17.leanback.widget.Presenter);
+ method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
+ method public void setProgressColor(int);
+ }
+
+ public class PlaybackTransportRowPresenter.ViewHolder extends android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder implements android.support.v17.leanback.widget.PlaybackSeekUi {
+ ctor public PlaybackTransportRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter);
+ method public final android.widget.TextView getCurrentPositionView();
+ method public final android.support.v17.leanback.widget.Presenter.ViewHolder getDescriptionViewHolder();
+ method public final android.widget.TextView getDurationView();
+ method protected void onSetCurrentPositionLabel(long);
+ method protected void onSetDurationLabel(long);
+ method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
+ }
+
+ public abstract class Presenter implements android.support.v17.leanback.widget.FacetProvider {
+ ctor public Presenter();
+ method protected static void cancelAnimationsRecursive(android.view.View);
+ method public final java.lang.Object getFacet(java.lang.Class<?>);
+ method public abstract void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, java.util.List<java.lang.Object>);
+ method public abstract android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public abstract void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void onViewAttachedToWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void onViewDetachedFromWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public final void setFacet(java.lang.Class<?>, java.lang.Object);
+ method public void setOnClickListener(android.support.v17.leanback.widget.Presenter.ViewHolder, android.view.View.OnClickListener);
+ }
+
+ public static class Presenter.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
+ ctor public Presenter.ViewHolder(android.view.View);
+ method public final java.lang.Object getFacet(java.lang.Class<?>);
+ method public final void setFacet(java.lang.Class<?>, java.lang.Object);
+ field public final android.view.View view;
+ }
+
+ public static abstract class Presenter.ViewHolderTask {
+ ctor public Presenter.ViewHolderTask();
+ method public void run(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ }
+
+ public abstract class PresenterSelector {
+ ctor public PresenterSelector();
+ method public abstract android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
+ method public android.support.v17.leanback.widget.Presenter[] getPresenters();
+ }
+
+ public abstract class PresenterSwitcher {
+ ctor public PresenterSwitcher();
+ method public void clear();
+ method public final android.view.ViewGroup getParentViewGroup();
+ method public void init(android.view.ViewGroup, android.support.v17.leanback.widget.PresenterSelector);
+ method protected abstract void insertView(android.view.View);
+ method protected void onViewSelected(android.view.View);
+ method public void select(java.lang.Object);
+ method protected void showView(android.view.View, boolean);
+ method public void unselect();
+ }
+
+ public class RecyclerViewParallax extends android.support.v17.leanback.widget.Parallax {
+ ctor public RecyclerViewParallax();
+ method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty createProperty(java.lang.String, int);
+ method public float getMaxValue();
+ method public android.support.v7.widget.RecyclerView getRecyclerView();
+ method public void setRecyclerView(android.support.v7.widget.RecyclerView);
+ }
+
+ public static final class RecyclerViewParallax.ChildPositionProperty extends android.support.v17.leanback.widget.Parallax.IntProperty {
+ method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty adapterPosition(int);
+ method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty fraction(float);
+ method public int getAdapterPosition();
+ method public float getFraction();
+ method public int getOffset();
+ method public int getViewId();
+ method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty offset(int);
+ method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty viewId(int);
+ }
+
+ public class Row {
+ ctor public Row(long, android.support.v17.leanback.widget.HeaderItem);
+ ctor public Row(android.support.v17.leanback.widget.HeaderItem);
+ ctor public Row();
+ method public final android.support.v17.leanback.widget.HeaderItem getHeaderItem();
+ method public final long getId();
+ method public boolean isRenderedAsRowView();
+ method public final void setHeaderItem(android.support.v17.leanback.widget.HeaderItem);
+ method public final void setId(long);
+ }
+
+ public class RowHeaderPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public RowHeaderPresenter();
+ method protected static float getFontDescent(android.widget.TextView, android.graphics.Paint);
+ method public int getSpaceUnderBaseline(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder);
+ method public boolean isNullItemVisibilityGone();
+ method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method protected void onSelectLevelChanged(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder);
+ method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void setNullItemVisibilityGone(boolean);
+ method public final void setSelectLevel(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, float);
+ }
+
+ public static class RowHeaderPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
+ ctor public RowHeaderPresenter.ViewHolder(android.view.View);
+ method public final float getSelectLevel();
+ }
+
+ public final class RowHeaderView extends android.widget.TextView {
+ ctor public RowHeaderView(android.content.Context);
+ ctor public RowHeaderView(android.content.Context, android.util.AttributeSet);
+ ctor public RowHeaderView(android.content.Context, android.util.AttributeSet, int);
+ }
+
+ public abstract class RowPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public RowPresenter();
+ method protected abstract android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
+ method protected void dispatchItemSelectedListener(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
+ method public void freeze(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
+ method public final android.support.v17.leanback.widget.RowHeaderPresenter getHeaderPresenter();
+ method public final android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public final boolean getSelectEffectEnabled();
+ method public final float getSelectLevel(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public final int getSyncActivatePolicy();
+ method protected void initializeRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ method protected boolean isClippingChildren();
+ method public boolean isUsingDefaultSelectEffect();
+ method protected void onBindRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder, java.lang.Object);
+ method public final void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public final android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method protected void onRowViewAttachedToWindow(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ method protected void onRowViewDetachedFromWindow(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ method protected void onRowViewExpanded(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
+ method protected void onRowViewSelected(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
+ method protected void onSelectLevelChanged(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ method protected void onUnbindRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
+ method public final void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public final void onViewAttachedToWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public final void onViewDetachedFromWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void setEntranceTransitionState(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
+ method public final void setHeaderPresenter(android.support.v17.leanback.widget.RowHeaderPresenter);
+ method public final void setRowViewExpanded(android.support.v17.leanback.widget.Presenter.ViewHolder, boolean);
+ method public final void setRowViewSelected(android.support.v17.leanback.widget.Presenter.ViewHolder, boolean);
+ method public final void setSelectEffectEnabled(boolean);
+ method public final void setSelectLevel(android.support.v17.leanback.widget.Presenter.ViewHolder, float);
+ method public final void setSyncActivatePolicy(int);
+ field public static final int SYNC_ACTIVATED_CUSTOM = 0; // 0x0
+ field public static final int SYNC_ACTIVATED_TO_EXPANDED = 1; // 0x1
+ field public static final int SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED = 3; // 0x3
+ field public static final int SYNC_ACTIVATED_TO_SELECTED = 2; // 0x2
+ }
+
+ public static class RowPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
+ ctor public RowPresenter.ViewHolder(android.view.View);
+ method public final android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder getHeaderViewHolder();
+ method public final android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
+ method public final android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public android.view.View.OnKeyListener getOnKeyListener();
+ method public final android.support.v17.leanback.widget.Row getRow();
+ method public final java.lang.Object getRowObject();
+ method public final float getSelectLevel();
+ method public java.lang.Object getSelectedItem();
+ method public android.support.v17.leanback.widget.Presenter.ViewHolder getSelectedItemViewHolder();
+ method public final boolean isExpanded();
+ method public final boolean isSelected();
+ method public final void setActivated(boolean);
+ method public final void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
+ method public final void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
+ method public void setOnKeyListener(android.view.View.OnKeyListener);
+ method public final void syncActivatedStatus(android.view.View);
+ field protected final android.support.v17.leanback.graphics.ColorOverlayDimmer mColorDimmer;
+ }
+
+ public class SearchBar extends android.widget.RelativeLayout {
+ ctor public SearchBar(android.content.Context);
+ ctor public SearchBar(android.content.Context, android.util.AttributeSet);
+ ctor public SearchBar(android.content.Context, android.util.AttributeSet, int);
+ method public void displayCompletions(java.util.List<java.lang.String>);
+ method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public java.lang.CharSequence getHint();
+ method public java.lang.String getTitle();
+ method public boolean isRecognizing();
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setPermissionListener(android.support.v17.leanback.widget.SearchBar.SearchBarPermissionListener);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSearchBarListener(android.support.v17.leanback.widget.SearchBar.SearchBarListener);
+ method public void setSearchQuery(java.lang.String);
+ method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
+ method public void setSpeechRecognizer(android.speech.SpeechRecognizer);
+ method public void setTitle(java.lang.String);
+ method public void startRecognition();
+ method public void stopRecognition();
+ }
+
+ public static abstract interface SearchBar.SearchBarListener {
+ method public abstract void onKeyboardDismiss(java.lang.String);
+ method public abstract void onSearchQueryChange(java.lang.String);
+ method public abstract void onSearchQuerySubmit(java.lang.String);
+ }
+
+ public static abstract interface SearchBar.SearchBarPermissionListener {
+ method public abstract void requestAudioPermission();
+ }
+
+ public class SearchEditText extends android.support.v17.leanback.widget.StreamingTextView {
+ ctor public SearchEditText(android.content.Context);
+ ctor public SearchEditText(android.content.Context, android.util.AttributeSet);
+ ctor public SearchEditText(android.content.Context, android.util.AttributeSet, int);
+ method public void setOnKeyboardDismissListener(android.support.v17.leanback.widget.SearchEditText.OnKeyboardDismissListener);
+ }
+
+ public static abstract interface SearchEditText.OnKeyboardDismissListener {
+ method public abstract void onKeyboardDismiss();
+ }
+
+ public class SearchOrbView extends android.widget.FrameLayout implements android.view.View.OnClickListener {
+ ctor public SearchOrbView(android.content.Context);
+ ctor public SearchOrbView(android.content.Context, android.util.AttributeSet);
+ ctor public SearchOrbView(android.content.Context, android.util.AttributeSet, int);
+ method public void enableOrbColorAnimation(boolean);
+ method public int getOrbColor();
+ method public android.support.v17.leanback.widget.SearchOrbView.Colors getOrbColors();
+ method public android.graphics.drawable.Drawable getOrbIcon();
+ method public void onClick(android.view.View);
+ method public void setOnOrbClickedListener(android.view.View.OnClickListener);
+ method public void setOrbColor(int);
+ method public deprecated void setOrbColor(int, int);
+ method public void setOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setOrbIcon(android.graphics.drawable.Drawable);
+ }
+
+ public static class SearchOrbView.Colors {
+ ctor public SearchOrbView.Colors(int);
+ ctor public SearchOrbView.Colors(int, int);
+ ctor public SearchOrbView.Colors(int, int, int);
+ method public static int getBrightColor(int);
+ field public int brightColor;
+ field public int color;
+ field public int iconColor;
+ }
+
+ public class SectionRow extends android.support.v17.leanback.widget.Row {
+ ctor public SectionRow(android.support.v17.leanback.widget.HeaderItem);
+ ctor public SectionRow(long, java.lang.String);
+ ctor public SectionRow(java.lang.String);
+ method public final boolean isRenderedAsRowView();
+ }
+
+ public class ShadowOverlayContainer extends android.widget.FrameLayout {
+ ctor public ShadowOverlayContainer(android.content.Context);
+ ctor public ShadowOverlayContainer(android.content.Context, android.util.AttributeSet);
+ ctor public ShadowOverlayContainer(android.content.Context, android.util.AttributeSet, int);
+ method public int getShadowType();
+ method public android.view.View getWrappedView();
+ method public deprecated void initialize(boolean, boolean);
+ method public deprecated void initialize(boolean, boolean, boolean);
+ method public static void prepareParentForShadow(android.view.ViewGroup);
+ method public void setOverlayColor(int);
+ method public void setShadowFocusLevel(float);
+ method public static boolean supportsDynamicShadow();
+ method public static boolean supportsShadow();
+ method public void useDynamicShadow();
+ method public void useDynamicShadow(float, float);
+ method public void useStaticShadow();
+ method public void wrap(android.view.View);
+ field public static final int SHADOW_DYNAMIC = 3; // 0x3
+ field public static final int SHADOW_NONE = 1; // 0x1
+ field public static final int SHADOW_STATIC = 2; // 0x2
+ }
+
+ public final class ShadowOverlayHelper {
+ method public android.support.v17.leanback.widget.ShadowOverlayContainer createShadowOverlayContainer(android.content.Context);
+ method public int getShadowType();
+ method public boolean needsOverlay();
+ method public boolean needsRoundedCorner();
+ method public boolean needsWrapper();
+ method public void onViewCreated(android.view.View);
+ method public void prepareParentForShadow(android.view.ViewGroup);
+ method public static void setNoneWrapperOverlayColor(android.view.View, int);
+ method public static void setNoneWrapperShadowFocusLevel(android.view.View, float);
+ method public void setOverlayColor(android.view.View, int);
+ method public void setShadowFocusLevel(android.view.View, float);
+ method public static boolean supportsDynamicShadow();
+ method public static boolean supportsForeground();
+ method public static boolean supportsRoundedCorner();
+ method public static boolean supportsShadow();
+ field public static final int SHADOW_DYNAMIC = 3; // 0x3
+ field public static final int SHADOW_NONE = 1; // 0x1
+ field public static final int SHADOW_STATIC = 2; // 0x2
+ }
+
+ public static final class ShadowOverlayHelper.Builder {
+ ctor public ShadowOverlayHelper.Builder();
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper build(android.content.Context);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder keepForegroundDrawable(boolean);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsOverlay(boolean);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsRoundedCorner(boolean);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsShadow(boolean);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder options(android.support.v17.leanback.widget.ShadowOverlayHelper.Options);
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder preferZOrder(boolean);
+ }
+
+ public static final class ShadowOverlayHelper.Options {
+ ctor public ShadowOverlayHelper.Options();
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Options dynamicShadowZ(float, float);
+ method public final float getDynamicShadowFocusedZ();
+ method public final float getDynamicShadowUnfocusedZ();
+ method public final int getRoundedCornerRadius();
+ method public android.support.v17.leanback.widget.ShadowOverlayHelper.Options roundedCornerRadius(int);
+ field public static final android.support.v17.leanback.widget.ShadowOverlayHelper.Options DEFAULT;
+ }
+
+ public final class SinglePresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
+ ctor public SinglePresenterSelector(android.support.v17.leanback.widget.Presenter);
+ method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
+ }
+
+ public class SparseArrayObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
+ ctor public SparseArrayObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
+ ctor public SparseArrayObjectAdapter(android.support.v17.leanback.widget.Presenter);
+ ctor public SparseArrayObjectAdapter();
+ method public void clear(int);
+ method public void clear();
+ method public java.lang.Object get(int);
+ method public int indexOf(java.lang.Object);
+ method public int indexOf(int);
+ method public java.lang.Object lookup(int);
+ method public void notifyArrayItemRangeChanged(int, int);
+ method public void set(int, java.lang.Object);
+ method public int size();
+ }
+
+ public class SpeechOrbView extends android.support.v17.leanback.widget.SearchOrbView {
+ ctor public SpeechOrbView(android.content.Context);
+ ctor public SpeechOrbView(android.content.Context, android.util.AttributeSet);
+ ctor public SpeechOrbView(android.content.Context, android.util.AttributeSet, int);
+ method public void setListeningOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setNotListeningOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setSoundLevel(int);
+ method public void showListening();
+ method public void showNotListening();
+ }
+
+ public abstract deprecated interface SpeechRecognitionCallback {
+ method public abstract void recognizeSpeech();
+ }
+
+ class StreamingTextView extends android.widget.EditText {
+ ctor public StreamingTextView(android.content.Context, android.util.AttributeSet);
+ ctor public StreamingTextView(android.content.Context, android.util.AttributeSet, int);
+ method public static boolean isLayoutRtl(android.view.View);
+ method public void reset();
+ method public void setFinalRecognizedText(java.lang.CharSequence);
+ method public void updateRecognizedText(java.lang.String, java.lang.String);
+ method public void updateRecognizedText(java.lang.String, java.util.List<java.lang.Float>);
+ }
+
+ public class TitleHelper {
+ ctor public TitleHelper(android.view.ViewGroup, android.view.View);
+ method public android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener getOnFocusSearchListener();
+ method public android.view.ViewGroup getSceneRoot();
+ method public android.view.View getTitleView();
+ method public void showTitle(boolean);
+ }
+
+ public class TitleView extends android.widget.FrameLayout implements android.support.v17.leanback.widget.TitleViewAdapter.Provider {
+ ctor public TitleView(android.content.Context);
+ ctor public TitleView(android.content.Context, android.util.AttributeSet);
+ ctor public TitleView(android.content.Context, android.util.AttributeSet, int);
+ method public void enableAnimation(boolean);
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
+ method public android.view.View getSearchAffordanceView();
+ method public java.lang.CharSequence getTitle();
+ method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnSearchClickedListener(android.view.View.OnClickListener);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setTitle(java.lang.CharSequence);
+ method public void updateComponentsVisibility(int);
+ }
+
+ public abstract class TitleViewAdapter {
+ ctor public TitleViewAdapter();
+ method public android.graphics.drawable.Drawable getBadgeDrawable();
+ method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
+ method public abstract android.view.View getSearchAffordanceView();
+ method public java.lang.CharSequence getTitle();
+ method public void setAnimationEnabled(boolean);
+ method public void setBadgeDrawable(android.graphics.drawable.Drawable);
+ method public void setOnSearchClickedListener(android.view.View.OnClickListener);
+ method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
+ method public void setTitle(java.lang.CharSequence);
+ method public void updateComponentsVisibility(int);
+ field public static final int BRANDING_VIEW_VISIBLE = 2; // 0x2
+ field public static final int FULL_VIEW_VISIBLE = 6; // 0x6
+ field public static final int SEARCH_VIEW_VISIBLE = 4; // 0x4
+ }
+
+ public static abstract interface TitleViewAdapter.Provider {
+ method public abstract android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
+ }
+
+ public class VerticalGridPresenter extends android.support.v17.leanback.widget.Presenter {
+ ctor public VerticalGridPresenter();
+ ctor public VerticalGridPresenter(int);
+ ctor public VerticalGridPresenter(int, boolean);
+ method public final boolean areChildRoundedCornersEnabled();
+ method protected android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder createGridViewHolder(android.view.ViewGroup);
+ method protected android.support.v17.leanback.widget.ShadowOverlayHelper.Options createShadowOverlayOptions();
+ method public final void enableChildRoundedCorners(boolean);
+ method public final int getFocusZoomFactor();
+ method public final boolean getKeepChildForeground();
+ method public int getNumberOfColumns();
+ method public final android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
+ method public final android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
+ method public final boolean getShadowEnabled();
+ method protected void initializeGridViewHolder(android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder);
+ method public final boolean isFocusDimmerUsed();
+ method public boolean isUsingDefaultShadow();
+ method public boolean isUsingZOrder(android.content.Context);
+ method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
+ method public final android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
+ method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
+ method public void setEntranceTransitionState(android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder, boolean);
+ method public final void setKeepChildForeground(boolean);
+ method public void setNumberOfColumns(int);
+ method public final void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
+ method public final void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
+ method public final void setShadowEnabled(boolean);
+ }
+
+ public static class VerticalGridPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
+ ctor public VerticalGridPresenter.ViewHolder(android.support.v17.leanback.widget.VerticalGridView);
+ method public android.support.v17.leanback.widget.VerticalGridView getGridView();
+ }
+
+ public class VerticalGridView extends android.support.v17.leanback.widget.BaseGridView {
+ ctor public VerticalGridView(android.content.Context);
+ ctor public VerticalGridView(android.content.Context, android.util.AttributeSet);
+ ctor public VerticalGridView(android.content.Context, android.util.AttributeSet, int);
+ method protected void initAttributes(android.content.Context, android.util.AttributeSet);
+ method public void setColumnWidth(int);
+ method public void setNumColumns(int);
+ }
+
+ public abstract interface ViewHolderTask {
+ method public abstract void run(android.support.v7.widget.RecyclerView.ViewHolder);
+ }
+
+}
+
+package android.support.v17.leanback.widget.picker {
+
+ public class Picker extends android.widget.FrameLayout {
+ ctor public Picker(android.content.Context, android.util.AttributeSet, int);
+ method public void addOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
+ method public float getActivatedVisibleItemCount();
+ method public android.support.v17.leanback.widget.picker.PickerColumn getColumnAt(int);
+ method public int getColumnsCount();
+ method protected int getPickerItemHeightPixels();
+ method public final int getPickerItemLayoutId();
+ method public final int getPickerItemTextViewId();
+ method public int getSelectedColumn();
+ method public final deprecated java.lang.CharSequence getSeparator();
+ method public final java.util.List<java.lang.CharSequence> getSeparators();
+ method public float getVisibleItemCount();
+ method public void onColumnValueChanged(int, int);
+ method public void removeOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
+ method public void setActivatedVisibleItemCount(float);
+ method public void setColumnAt(int, android.support.v17.leanback.widget.picker.PickerColumn);
+ method public void setColumnValue(int, int, boolean);
+ method public void setColumns(java.util.List<android.support.v17.leanback.widget.picker.PickerColumn>);
+ method public final void setPickerItemTextViewId(int);
+ method public void setSelectedColumn(int);
+ method public final void setSeparator(java.lang.CharSequence);
+ method public final void setSeparators(java.util.List<java.lang.CharSequence>);
+ method public void setVisibleItemCount(float);
+ }
+
+ public static abstract interface Picker.PickerValueListener {
+ method public abstract void onValueChanged(android.support.v17.leanback.widget.picker.Picker, int);
+ }
+
+ public class PickerColumn {
+ ctor public PickerColumn();
+ method public int getCount();
+ method public int getCurrentValue();
+ method public java.lang.CharSequence getLabelFor(int);
+ method public java.lang.String getLabelFormat();
+ method public int getMaxValue();
+ method public int getMinValue();
+ method public java.lang.CharSequence[] getStaticLabels();
+ method public void setCurrentValue(int);
+ method public void setLabelFormat(java.lang.String);
+ method public void setMaxValue(int);
+ method public void setMinValue(int);
+ method public void setStaticLabels(java.lang.CharSequence[]);
+ }
+
+ public class TimePicker extends android.support.v17.leanback.widget.picker.Picker {
+ ctor public TimePicker(android.content.Context, android.util.AttributeSet);
+ ctor public TimePicker(android.content.Context, android.util.AttributeSet, int);
+ method public int getHour();
+ method public int getMinute();
+ method public boolean is24Hour();
+ method public boolean isPm();
+ method public void setHour(int);
+ method public void setIs24Hour(boolean);
+ method public void setMinute(int);
+ }
+
+}
+
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java b/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
rename to leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java b/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
rename to leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java b/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java b/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
rename to leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java b/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java b/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
diff --git a/v17/leanback/build.gradle b/leanback/build.gradle
similarity index 100%
rename from v17/leanback/build.gradle
rename to leanback/build.gradle
diff --git a/v17/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java b/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
similarity index 100%
rename from v17/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
rename to leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
diff --git a/v17/leanback/common/android/support/v17/leanback/transition/TransitionListener.java b/leanback/common/android/support/v17/leanback/transition/TransitionListener.java
similarity index 100%
rename from v17/leanback/common/android/support/v17/leanback/transition/TransitionListener.java
rename to leanback/common/android/support/v17/leanback/transition/TransitionListener.java
diff --git a/leanback/generatef.py b/leanback/generatef.py
new file mode 100755
index 0000000..6364f09
--- /dev/null
+++ b/leanback/generatef.py
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import re
+
+print "Generate framework fragment related code for leanback"
+
+cls = ['Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
+ 'Playback', 'Rows', 'Search', 'VerticalGrid', 'Branded',
+ 'GuidedStep', 'Onboarding', 'Video']
+
+for w in cls:
+ print "copy {}SupportFragment to {}Fragment".format(w, w)
+
+ file = open('src/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'r')
+ content = "// CHECKSTYLE:OFF Generated code\n"
+ content = content + "/* This file is auto-generated from {}SupportFragment.java. DO NOT MODIFY. */\n\n".format(w)
+
+ for line in file:
+ line = line.replace('IS_FRAMEWORK_FRAGMENT = false', 'IS_FRAMEWORK_FRAGMENT = true');
+ for w2 in cls:
+ line = line.replace('{}SupportFragment'.format(w2), '{}Fragment'.format(w2))
+ line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('activity.getSupportFragmentManager()', 'activity.getFragmentManager()')
+ line = line.replace('FragmentActivity activity', 'Activity activity')
+ line = line.replace('(FragmentActivity', '(Activity')
+ # replace getContext() with FragmentUtil.getContext(XXXFragment.this), but dont match the case "view.getContext()"
+ line = re.sub(r'([^\.])getContext\(\)', r'\1FragmentUtil.getContext({}Fragment.this)'.format(w), line);
+ content = content + line
+ file.close()
+ # add deprecated tag to fragment class and inner classes/interfaces
+ content = re.sub(r'\*\/\n(@.*\n|)(public |abstract public |abstract |)class', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n@Deprecated\n\\1\\2class', content)
+ content = re.sub(r'\*\/\n public (static class|interface|final static class|abstract static class)', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n @Deprecated\n public \\1', content)
+ outfile = open('src/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'w')
+ outfile.write(content)
+ outfile.close()
+
+
+
+print "copy VideoSupportFragmentGlueHost to VideoFragmentGlueHost"
+file = open('src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java', 'r')
+content = "// CHECKSTYLE:OFF Generated code\n"
+content = content + "/* This file is auto-generated from VideoSupportFragmentGlueHost.java. DO NOT MODIFY. */\n\n"
+for line in file:
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('VideoSupportFragment', 'VideoFragment')
+ line = line.replace('PlaybackSupportFragment', 'PlaybackFragment')
+ content = content + line
+file.close()
+# add deprecated tag to class
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link VideoSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
+outfile = open('src/android/support/v17/leanback/app/VideoFragmentGlueHost.java', 'w')
+outfile.write(content)
+outfile.close()
+
+
+
+print "copy PlaybackSupportFragmentGlueHost to PlaybackFragmentGlueHost"
+file = open('src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java', 'r')
+content = "// CHECKSTYLE:OFF Generated code\n"
+content = content + "/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java. DO NOT MODIFY. */\n\n"
+for line in file:
+ line = line.replace('VideoSupportFragment', 'VideoFragment')
+ line = line.replace('PlaybackSupportFragment', 'PlaybackFragment')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ content = content + line
+file.close()
+# add deprecated tag to class
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link PlaybackSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
+outfile = open('src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java', 'w')
+outfile.write(content)
+outfile.close()
+
+
+
+print "copy DetailsSupportFragmentBackgroundController to DetailsFragmentBackgroundController"
+file = open('src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java', 'r')
+content = "// CHECKSTYLE:OFF Generated code\n"
+content = content + "/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java. DO NOT MODIFY. */\n\n"
+for line in file:
+ line = line.replace('VideoSupportFragment', 'VideoFragment')
+ line = line.replace('DetailsSupportFragment', 'DetailsFragment')
+ line = line.replace('RowsSupportFragment', 'RowsFragment')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('mFragment.getContext()', 'FragmentUtil.getContext(mFragment)')
+ content = content + line
+file.close()
+# add deprecated tag to class
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link DetailsSupportFragmentBackgroundController}\n */\n@Deprecated\npublic class', content)
+outfile = open('src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java', 'w')
+outfile.write(content)
+outfile.close()
diff --git a/v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java b/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
similarity index 100%
rename from v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
rename to leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java b/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/Scale.java b/leanback/kitkat/android/support/v17/leanback/transition/Scale.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/Scale.java
rename to leanback/kitkat/android/support/v17/leanback/transition/Scale.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java b/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java b/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
diff --git a/v17/leanback/lint-baseline.xml b/leanback/lint-baseline.xml
similarity index 100%
rename from v17/leanback/lint-baseline.xml
rename to leanback/lint-baseline.xml
diff --git a/v17/leanback/res/anim/lb_decelerator_2.xml b/leanback/res/anim/lb_decelerator_2.xml
similarity index 100%
rename from v17/leanback/res/anim/lb_decelerator_2.xml
rename to leanback/res/anim/lb_decelerator_2.xml
diff --git a/v17/leanback/res/anim/lb_decelerator_4.xml b/leanback/res/anim/lb_decelerator_4.xml
similarity index 100%
rename from v17/leanback/res/anim/lb_decelerator_4.xml
rename to leanback/res/anim/lb_decelerator_4.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_description_enter.xml b/leanback/res/animator-v21/lb_onboarding_description_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_description_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_description_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_logo_enter.xml b/leanback/res/animator-v21/lb_onboarding_logo_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_logo_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_logo_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_logo_exit.xml b/leanback/res/animator-v21/lb_onboarding_logo_exit.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_logo_exit.xml
rename to leanback/res/animator-v21/lb_onboarding_logo_exit.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml b/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_title_enter.xml b/leanback/res/animator-v21/lb_onboarding_title_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_title_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_title_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_bg_fade_in.xml b/leanback/res/animator-v21/lb_playback_bg_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_bg_fade_in.xml
rename to leanback/res/animator-v21/lb_playback_bg_fade_in.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_bg_fade_out.xml b/leanback/res/animator-v21/lb_playback_bg_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_bg_fade_out.xml
rename to leanback/res/animator-v21/lb_playback_bg_fade_out.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_description_fade_out.xml b/leanback/res/animator-v21/lb_playback_description_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_description_fade_out.xml
rename to leanback/res/animator-v21/lb_playback_description_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_pressed.xml b/leanback/res/animator/lb_guidedactions_item_pressed.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedactions_item_pressed.xml
rename to leanback/res/animator/lb_guidedactions_item_pressed.xml
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml b/leanback/res/animator/lb_guidedactions_item_unpressed.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml
rename to leanback/res/animator/lb_guidedactions_item_unpressed.xml
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_down.xml b/leanback/res/animator/lb_guidedstep_slide_down.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedstep_slide_down.xml
rename to leanback/res/animator/lb_guidedstep_slide_down.xml
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_up.xml b/leanback/res/animator/lb_guidedstep_slide_up.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedstep_slide_up.xml
rename to leanback/res/animator/lb_guidedstep_slide_up.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_description_enter.xml b/leanback/res/animator/lb_onboarding_description_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_description_enter.xml
rename to leanback/res/animator/lb_onboarding_description_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_logo_enter.xml b/leanback/res/animator/lb_onboarding_logo_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_logo_enter.xml
rename to leanback/res/animator/lb_onboarding_logo_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_logo_exit.xml b/leanback/res/animator/lb_onboarding_logo_exit.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_logo_exit.xml
rename to leanback/res/animator/lb_onboarding_logo_exit.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_enter.xml b/leanback/res/animator/lb_onboarding_page_indicator_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_enter.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml b/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml b/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_start_button_fade_in.xml b/leanback/res/animator/lb_onboarding_start_button_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_start_button_fade_in.xml
rename to leanback/res/animator/lb_onboarding_start_button_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_start_button_fade_out.xml b/leanback/res/animator/lb_onboarding_start_button_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_start_button_fade_out.xml
rename to leanback/res/animator/lb_onboarding_start_button_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_title_enter.xml b/leanback/res/animator/lb_onboarding_title_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_title_enter.xml
rename to leanback/res/animator/lb_onboarding_title_enter.xml
diff --git a/v17/leanback/res/animator/lb_playback_bg_fade_in.xml b/leanback/res/animator/lb_playback_bg_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_bg_fade_in.xml
rename to leanback/res/animator/lb_playback_bg_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_bg_fade_out.xml b/leanback/res/animator/lb_playback_bg_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_bg_fade_out.xml
rename to leanback/res/animator/lb_playback_bg_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_controls_fade_in.xml b/leanback/res/animator/lb_playback_controls_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_controls_fade_in.xml
rename to leanback/res/animator/lb_playback_controls_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_controls_fade_out.xml b/leanback/res/animator/lb_playback_controls_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_controls_fade_out.xml
rename to leanback/res/animator/lb_playback_controls_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_description_fade_in.xml b/leanback/res/animator/lb_playback_description_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_description_fade_in.xml
rename to leanback/res/animator/lb_playback_description_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_description_fade_out.xml b/leanback/res/animator/lb_playback_description_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_description_fade_out.xml
rename to leanback/res/animator/lb_playback_description_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_rows_fade_in.xml b/leanback/res/animator/lb_playback_rows_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_rows_fade_in.xml
rename to leanback/res/animator/lb_playback_rows_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_rows_fade_out.xml b/leanback/res/animator/lb_playback_rows_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_rows_fade_out.xml
rename to leanback/res/animator/lb_playback_rows_fade_out.xml
diff --git a/v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png b/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-hdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png b/leanback/res/drawable-hdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-hdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png b/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-mdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png b/leanback/res/drawable-mdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-mdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-v21/lb_action_bg.xml b/leanback/res/drawable-v21/lb_action_bg.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_action_bg.xml
rename to leanback/res/drawable-v21/lb_action_bg.xml
diff --git a/v17/leanback/res/drawable-v21/lb_card_foreground.xml b/leanback/res/drawable-v21/lb_card_foreground.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_card_foreground.xml
rename to leanback/res/drawable-v21/lb_card_foreground.xml
diff --git a/v17/leanback/res/drawable-v21/lb_control_button_primary.xml b/leanback/res/drawable-v21/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_control_button_primary.xml
rename to leanback/res/drawable-v21/lb_control_button_primary.xml
diff --git a/v17/leanback/res/drawable-v21/lb_control_button_secondary.xml b/leanback/res/drawable-v21/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_control_button_secondary.xml
rename to leanback/res/drawable-v21/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml b/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
rename to leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
diff --git a/v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png b/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png b/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
rename to leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_cc.png b/leanback/res/drawable-xhdpi/lb_ic_cc.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_cc.png
rename to leanback/res/drawable-xhdpi/lb_ic_cc.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png b/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
rename to leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png b/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
rename to leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png b/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
rename to leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_hq.png b/leanback/res/drawable-xhdpi/lb_ic_hq.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_hq.png
rename to leanback/res/drawable-xhdpi/lb_ic_hq.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png b/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_loop.png b/leanback/res/drawable-xhdpi/lb_ic_loop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_loop.png
rename to leanback/res/drawable-xhdpi/lb_ic_loop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_loop_one.png b/leanback/res/drawable-xhdpi/lb_ic_loop_one.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_loop_one.png
rename to leanback/res/drawable-xhdpi/lb_ic_loop_one.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_more.png b/leanback/res/drawable-xhdpi/lb_ic_more.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_more.png
rename to leanback/res/drawable-xhdpi/lb_ic_more.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png b/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
rename to leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_pause.png b/leanback/res/drawable-xhdpi/lb_ic_pause.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_pause.png
rename to leanback/res/drawable-xhdpi/lb_ic_pause.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_pip.png b/leanback/res/drawable-xhdpi/lb_ic_pip.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_pip.png
rename to leanback/res/drawable-xhdpi/lb_ic_pip.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_play.png b/leanback/res/drawable-xhdpi/lb_ic_play.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_play.png
rename to leanback/res/drawable-xhdpi/lb_ic_play.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_play_fit.png b/leanback/res/drawable-xhdpi/lb_ic_play_fit.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_play_fit.png
rename to leanback/res/drawable-xhdpi/lb_ic_play_fit.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png b/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
rename to leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_replay.png b/leanback/res/drawable-xhdpi/lb_ic_replay.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_replay.png
rename to leanback/res/drawable-xhdpi/lb_ic_replay.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png b/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-xhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_shuffle.png b/leanback/res/drawable-xhdpi/lb_ic_shuffle.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_shuffle.png
rename to leanback/res/drawable-xhdpi/lb_ic_shuffle.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_skip_next.png b/leanback/res/drawable-xhdpi/lb_ic_skip_next.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_skip_next.png
rename to leanback/res/drawable-xhdpi/lb_ic_skip_next.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png b/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
rename to leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_stop.png b/leanback/res/drawable-xhdpi/lb_ic_stop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_stop.png
rename to leanback/res/drawable-xhdpi/lb_ic_stop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_one.png b/leanback/res/drawable-xhdpi/lb_text_dot_one.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_one.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_one.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png b/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_two.png b/leanback/res/drawable-xhdpi/lb_text_dot_two.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_two.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_two.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png b/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png b/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png b/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable/lb_background.xml b/leanback/res/drawable/lb_background.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_background.xml
rename to leanback/res/drawable/lb_background.xml
diff --git a/v17/leanback/res/drawable/lb_card_foreground.xml b/leanback/res/drawable/lb_card_foreground.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_card_foreground.xml
rename to leanback/res/drawable/lb_card_foreground.xml
diff --git a/v17/leanback/res/drawable/lb_control_button_primary.xml b/leanback/res/drawable/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_control_button_primary.xml
rename to leanback/res/drawable/lb_control_button_primary.xml
diff --git a/v17/leanback/res/drawable/lb_control_button_secondary.xml b/leanback/res/drawable/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_control_button_secondary.xml
rename to leanback/res/drawable/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/drawable/lb_headers_right_fading.xml b/leanback/res/drawable/lb_headers_right_fading.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_headers_right_fading.xml
rename to leanback/res/drawable/lb_headers_right_fading.xml
diff --git a/v17/leanback/res/drawable/lb_onboarding_start_button_background.xml b/leanback/res/drawable/lb_onboarding_start_button_background.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_onboarding_start_button_background.xml
rename to leanback/res/drawable/lb_onboarding_start_button_background.xml
diff --git a/v17/leanback/res/drawable/lb_playback_now_playing_bar.xml b/leanback/res/drawable/lb_playback_now_playing_bar.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_playback_now_playing_bar.xml
rename to leanback/res/drawable/lb_playback_now_playing_bar.xml
diff --git a/v17/leanback/res/drawable/lb_playback_progress_bar.xml b/leanback/res/drawable/lb_playback_progress_bar.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_playback_progress_bar.xml
rename to leanback/res/drawable/lb_playback_progress_bar.xml
diff --git a/v17/leanback/res/drawable/lb_search_orb.xml b/leanback/res/drawable/lb_search_orb.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_search_orb.xml
rename to leanback/res/drawable/lb_search_orb.xml
diff --git a/v17/leanback/res/drawable/lb_speech_orb.xml b/leanback/res/drawable/lb_speech_orb.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_speech_orb.xml
rename to leanback/res/drawable/lb_speech_orb.xml
diff --git a/v17/leanback/res/layout/lb_action_1_line.xml b/leanback/res/layout/lb_action_1_line.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_action_1_line.xml
rename to leanback/res/layout/lb_action_1_line.xml
diff --git a/v17/leanback/res/layout/lb_action_2_lines.xml b/leanback/res/layout/lb_action_2_lines.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_action_2_lines.xml
rename to leanback/res/layout/lb_action_2_lines.xml
diff --git a/v17/leanback/res/layout/lb_background_window.xml b/leanback/res/layout/lb_background_window.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_background_window.xml
rename to leanback/res/layout/lb_background_window.xml
diff --git a/v17/leanback/res/layout/lb_browse_fragment.xml b/leanback/res/layout/lb_browse_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_browse_fragment.xml
rename to leanback/res/layout/lb_browse_fragment.xml
diff --git a/v17/leanback/res/layout/lb_browse_title.xml b/leanback/res/layout/lb_browse_title.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_browse_title.xml
rename to leanback/res/layout/lb_browse_title.xml
diff --git a/v17/leanback/res/layout/lb_control_bar.xml b/leanback/res/layout/lb_control_bar.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_bar.xml
rename to leanback/res/layout/lb_control_bar.xml
diff --git a/v17/leanback/res/layout/lb_control_button_primary.xml b/leanback/res/layout/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_button_primary.xml
rename to leanback/res/layout/lb_control_button_primary.xml
diff --git a/v17/leanback/res/layout/lb_control_button_secondary.xml b/leanback/res/layout/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_button_secondary.xml
rename to leanback/res/layout/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/layout/lb_details_description.xml b/leanback/res/layout/lb_details_description.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_description.xml
rename to leanback/res/layout/lb_details_description.xml
diff --git a/v17/leanback/res/layout/lb_details_fragment.xml b/leanback/res/layout/lb_details_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_fragment.xml
rename to leanback/res/layout/lb_details_fragment.xml
diff --git a/v17/leanback/res/layout/lb_details_overview.xml b/leanback/res/layout/lb_details_overview.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_overview.xml
rename to leanback/res/layout/lb_details_overview.xml
diff --git a/v17/leanback/res/layout/lb_divider.xml b/leanback/res/layout/lb_divider.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_divider.xml
rename to leanback/res/layout/lb_divider.xml
diff --git a/v17/leanback/res/layout/lb_error_fragment.xml b/leanback/res/layout/lb_error_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_error_fragment.xml
rename to leanback/res/layout/lb_error_fragment.xml
diff --git a/v17/leanback/res/layout/lb_fullwidth_details_overview.xml b/leanback/res/layout/lb_fullwidth_details_overview.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_fullwidth_details_overview.xml
rename to leanback/res/layout/lb_fullwidth_details_overview.xml
diff --git a/v17/leanback/res/layout/lb_fullwidth_details_overview_logo.xml b/leanback/res/layout/lb_fullwidth_details_overview_logo.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_fullwidth_details_overview_logo.xml
rename to leanback/res/layout/lb_fullwidth_details_overview_logo.xml
diff --git a/v17/leanback/res/layout/lb_guidance.xml b/leanback/res/layout/lb_guidance.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidance.xml
rename to leanback/res/layout/lb_guidance.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions.xml b/leanback/res/layout/lb_guidedactions.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions.xml
rename to leanback/res/layout/lb_guidedactions.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml b/leanback/res/layout/lb_guidedactions_datepicker_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml
rename to leanback/res/layout/lb_guidedactions_datepicker_item.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions_item.xml b/leanback/res/layout/lb_guidedactions_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions_item.xml
rename to leanback/res/layout/lb_guidedactions_item.xml
diff --git a/v17/leanback/res/layout/lb_guidedbuttonactions.xml b/leanback/res/layout/lb_guidedbuttonactions.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedbuttonactions.xml
rename to leanback/res/layout/lb_guidedbuttonactions.xml
diff --git a/v17/leanback/res/layout/lb_guidedstep_background.xml b/leanback/res/layout/lb_guidedstep_background.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedstep_background.xml
rename to leanback/res/layout/lb_guidedstep_background.xml
diff --git a/v17/leanback/res/layout/lb_guidedstep_fragment.xml b/leanback/res/layout/lb_guidedstep_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedstep_fragment.xml
rename to leanback/res/layout/lb_guidedstep_fragment.xml
diff --git a/v17/leanback/res/layout/lb_header.xml b/leanback/res/layout/lb_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_header.xml
rename to leanback/res/layout/lb_header.xml
diff --git a/v17/leanback/res/layout/lb_headers_fragment.xml b/leanback/res/layout/lb_headers_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_headers_fragment.xml
rename to leanback/res/layout/lb_headers_fragment.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view.xml b/leanback/res/layout/lb_image_card_view.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view.xml
rename to leanback/res/layout/lb_image_card_view.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_badge_left.xml b/leanback/res/layout/lb_image_card_view_themed_badge_left.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_badge_left.xml
rename to leanback/res/layout/lb_image_card_view_themed_badge_left.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_badge_right.xml b/leanback/res/layout/lb_image_card_view_themed_badge_right.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_badge_right.xml
rename to leanback/res/layout/lb_image_card_view_themed_badge_right.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_content.xml b/leanback/res/layout/lb_image_card_view_themed_content.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_content.xml
rename to leanback/res/layout/lb_image_card_view_themed_content.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_title.xml b/leanback/res/layout/lb_image_card_view_themed_title.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_title.xml
rename to leanback/res/layout/lb_image_card_view_themed_title.xml
diff --git a/v17/leanback/res/layout/lb_list_row.xml b/leanback/res/layout/lb_list_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_list_row.xml
rename to leanback/res/layout/lb_list_row.xml
diff --git a/v17/leanback/res/layout/lb_list_row_hovercard.xml b/leanback/res/layout/lb_list_row_hovercard.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_list_row_hovercard.xml
rename to leanback/res/layout/lb_list_row_hovercard.xml
diff --git a/v17/leanback/res/layout/lb_media_item_number_view_flipper.xml b/leanback/res/layout/lb_media_item_number_view_flipper.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_media_item_number_view_flipper.xml
rename to leanback/res/layout/lb_media_item_number_view_flipper.xml
diff --git a/v17/leanback/res/layout/lb_media_list_header.xml b/leanback/res/layout/lb_media_list_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_media_list_header.xml
rename to leanback/res/layout/lb_media_list_header.xml
diff --git a/v17/leanback/res/layout/lb_onboarding_fragment.xml b/leanback/res/layout/lb_onboarding_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_onboarding_fragment.xml
rename to leanback/res/layout/lb_onboarding_fragment.xml
diff --git a/v17/leanback/res/layout/lb_picker.xml b/leanback/res/layout/lb_picker.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker.xml
rename to leanback/res/layout/lb_picker.xml
diff --git a/v17/leanback/res/layout/lb_picker_column.xml b/leanback/res/layout/lb_picker_column.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_column.xml
rename to leanback/res/layout/lb_picker_column.xml
diff --git a/v17/leanback/res/layout/lb_picker_item.xml b/leanback/res/layout/lb_picker_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_item.xml
rename to leanback/res/layout/lb_picker_item.xml
diff --git a/v17/leanback/res/layout/lb_picker_separator.xml b/leanback/res/layout/lb_picker_separator.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_separator.xml
rename to leanback/res/layout/lb_picker_separator.xml
diff --git a/v17/leanback/res/layout/lb_playback_controls.xml b/leanback/res/layout/lb_playback_controls.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_controls.xml
rename to leanback/res/layout/lb_playback_controls.xml
diff --git a/v17/leanback/res/layout/lb_playback_controls_row.xml b/leanback/res/layout/lb_playback_controls_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_controls_row.xml
rename to leanback/res/layout/lb_playback_controls_row.xml
diff --git a/v17/leanback/res/layout/lb_playback_fragment.xml b/leanback/res/layout/lb_playback_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_fragment.xml
rename to leanback/res/layout/lb_playback_fragment.xml
diff --git a/v17/leanback/res/layout/lb_playback_now_playing_bars.xml b/leanback/res/layout/lb_playback_now_playing_bars.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_now_playing_bars.xml
rename to leanback/res/layout/lb_playback_now_playing_bars.xml
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls.xml b/leanback/res/layout/lb_playback_transport_controls.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_transport_controls.xml
rename to leanback/res/layout/lb_playback_transport_controls.xml
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls_row.xml b/leanback/res/layout/lb_playback_transport_controls_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_transport_controls_row.xml
rename to leanback/res/layout/lb_playback_transport_controls_row.xml
diff --git a/v17/leanback/res/layout/lb_row_container.xml b/leanback/res/layout/lb_row_container.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_container.xml
rename to leanback/res/layout/lb_row_container.xml
diff --git a/v17/leanback/res/layout/lb_row_header.xml b/leanback/res/layout/lb_row_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_header.xml
rename to leanback/res/layout/lb_row_header.xml
diff --git a/v17/leanback/res/layout/lb_row_media_item.xml b/leanback/res/layout/lb_row_media_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_media_item.xml
rename to leanback/res/layout/lb_row_media_item.xml
diff --git a/v17/leanback/res/layout/lb_row_media_item_action.xml b/leanback/res/layout/lb_row_media_item_action.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_media_item_action.xml
rename to leanback/res/layout/lb_row_media_item_action.xml
diff --git a/v17/leanback/res/layout/lb_rows_fragment.xml b/leanback/res/layout/lb_rows_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_rows_fragment.xml
rename to leanback/res/layout/lb_rows_fragment.xml
diff --git a/v17/leanback/res/layout/lb_search_bar.xml b/leanback/res/layout/lb_search_bar.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_bar.xml
rename to leanback/res/layout/lb_search_bar.xml
diff --git a/v17/leanback/res/layout/lb_search_fragment.xml b/leanback/res/layout/lb_search_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_fragment.xml
rename to leanback/res/layout/lb_search_fragment.xml
diff --git a/v17/leanback/res/layout/lb_search_orb.xml b/leanback/res/layout/lb_search_orb.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_orb.xml
rename to leanback/res/layout/lb_search_orb.xml
diff --git a/v17/leanback/res/layout/lb_section_header.xml b/leanback/res/layout/lb_section_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_section_header.xml
rename to leanback/res/layout/lb_section_header.xml
diff --git a/v17/leanback/res/layout/lb_shadow.xml b/leanback/res/layout/lb_shadow.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_shadow.xml
rename to leanback/res/layout/lb_shadow.xml
diff --git a/v17/leanback/res/layout/lb_speech_orb.xml b/leanback/res/layout/lb_speech_orb.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_speech_orb.xml
rename to leanback/res/layout/lb_speech_orb.xml
diff --git a/v17/leanback/res/layout/lb_title_view.xml b/leanback/res/layout/lb_title_view.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_title_view.xml
rename to leanback/res/layout/lb_title_view.xml
diff --git a/v17/leanback/res/layout/lb_vertical_grid.xml b/leanback/res/layout/lb_vertical_grid.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_vertical_grid.xml
rename to leanback/res/layout/lb_vertical_grid.xml
diff --git a/v17/leanback/res/layout/lb_vertical_grid_fragment.xml b/leanback/res/layout/lb_vertical_grid_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_vertical_grid_fragment.xml
rename to leanback/res/layout/lb_vertical_grid_fragment.xml
diff --git a/v17/leanback/res/layout/lb_video_surface.xml b/leanback/res/layout/lb_video_surface.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_video_surface.xml
rename to leanback/res/layout/lb_video_surface.xml
diff --git a/v17/leanback/res/layout/video_surface_fragment.xml b/leanback/res/layout/video_surface_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/video_surface_fragment.xml
rename to leanback/res/layout/video_surface_fragment.xml
diff --git a/v17/leanback/res/raw/lb_voice_failure.ogg b/leanback/res/raw/lb_voice_failure.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_failure.ogg
rename to leanback/res/raw/lb_voice_failure.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_no_input.ogg b/leanback/res/raw/lb_voice_no_input.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_no_input.ogg
rename to leanback/res/raw/lb_voice_no_input.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_open.ogg b/leanback/res/raw/lb_voice_open.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_open.ogg
rename to leanback/res/raw/lb_voice_open.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_success.ogg b/leanback/res/raw/lb_voice_success.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_success.ogg
rename to leanback/res/raw/lb_voice_success.ogg
Binary files differ
diff --git a/v17/leanback/res/transition-v19/lb_browse_headers_in.xml b/leanback/res/transition-v19/lb_browse_headers_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v19/lb_browse_headers_in.xml
rename to leanback/res/transition-v19/lb_browse_headers_in.xml
diff --git a/v17/leanback/res/transition-v19/lb_browse_headers_out.xml b/leanback/res/transition-v19/lb_browse_headers_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v19/lb_browse_headers_out.xml
rename to leanback/res/transition-v19/lb_browse_headers_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_enter_transition.xml b/leanback/res/transition-v21/lb_browse_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_enter_transition.xml
rename to leanback/res/transition-v21/lb_browse_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_entrance_transition.xml b/leanback/res/transition-v21/lb_browse_entrance_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_entrance_transition.xml
rename to leanback/res/transition-v21/lb_browse_entrance_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_headers_in.xml b/leanback/res/transition-v21/lb_browse_headers_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_headers_in.xml
rename to leanback/res/transition-v21/lb_browse_headers_in.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_headers_out.xml b/leanback/res/transition-v21/lb_browse_headers_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_headers_out.xml
rename to leanback/res/transition-v21/lb_browse_headers_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_return_transition.xml b/leanback/res/transition-v21/lb_browse_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_return_transition.xml
rename to leanback/res/transition-v21/lb_browse_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml b/leanback/res/transition-v21/lb_details_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_details_enter_transition.xml
rename to leanback/res/transition-v21/lb_details_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_details_return_transition.xml b/leanback/res/transition-v21/lb_details_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_details_return_transition.xml
rename to leanback/res/transition-v21/lb_details_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_enter_transition.xml b/leanback/res/transition-v21/lb_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_enter_transition.xml
rename to leanback/res/transition-v21/lb_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml b/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
rename to leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
diff --git a/v17/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml b/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
rename to leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
diff --git a/v17/leanback/res/transition-v21/lb_return_transition.xml b/leanback/res/transition-v21/lb_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_return_transition.xml
rename to leanback/res/transition-v21/lb_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_shared_element_enter_transition.xml b/leanback/res/transition-v21/lb_shared_element_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_shared_element_enter_transition.xml
rename to leanback/res/transition-v21/lb_shared_element_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_shared_element_return_transition.xml b/leanback/res/transition-v21/lb_shared_element_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_shared_element_return_transition.xml
rename to leanback/res/transition-v21/lb_shared_element_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_title_in.xml b/leanback/res/transition-v21/lb_title_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_title_in.xml
rename to leanback/res/transition-v21/lb_title_in.xml
diff --git a/v17/leanback/res/transition-v21/lb_title_out.xml b/leanback/res/transition-v21/lb_title_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_title_out.xml
rename to leanback/res/transition-v21/lb_title_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
diff --git a/v17/leanback/res/values-af/strings.xml b/leanback/res/values-af/strings.xml
similarity index 100%
rename from v17/leanback/res/values-af/strings.xml
rename to leanback/res/values-af/strings.xml
diff --git a/v17/leanback/res/values-am/strings.xml b/leanback/res/values-am/strings.xml
similarity index 100%
rename from v17/leanback/res/values-am/strings.xml
rename to leanback/res/values-am/strings.xml
diff --git a/v17/leanback/res/values-ar/strings.xml b/leanback/res/values-ar/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ar/strings.xml
rename to leanback/res/values-ar/strings.xml
diff --git a/v17/leanback/res/values-az/strings.xml b/leanback/res/values-az/strings.xml
similarity index 100%
rename from v17/leanback/res/values-az/strings.xml
rename to leanback/res/values-az/strings.xml
diff --git a/v17/leanback/res/values-b+sr+Latn/strings.xml b/leanback/res/values-b+sr+Latn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-b+sr+Latn/strings.xml
rename to leanback/res/values-b+sr+Latn/strings.xml
diff --git a/v17/leanback/res/values-be/strings.xml b/leanback/res/values-be/strings.xml
similarity index 100%
rename from v17/leanback/res/values-be/strings.xml
rename to leanback/res/values-be/strings.xml
diff --git a/v17/leanback/res/values-bg/strings.xml b/leanback/res/values-bg/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bg/strings.xml
rename to leanback/res/values-bg/strings.xml
diff --git a/v17/leanback/res/values-bn/strings.xml b/leanback/res/values-bn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bn/strings.xml
rename to leanback/res/values-bn/strings.xml
diff --git a/v17/leanback/res/values-bs/strings.xml b/leanback/res/values-bs/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bs/strings.xml
rename to leanback/res/values-bs/strings.xml
diff --git a/v17/leanback/res/values-ca/strings.xml b/leanback/res/values-ca/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ca/strings.xml
rename to leanback/res/values-ca/strings.xml
diff --git a/v17/leanback/res/values-cs/strings.xml b/leanback/res/values-cs/strings.xml
similarity index 100%
rename from v17/leanback/res/values-cs/strings.xml
rename to leanback/res/values-cs/strings.xml
diff --git a/v17/leanback/res/values-da/strings.xml b/leanback/res/values-da/strings.xml
similarity index 100%
rename from v17/leanback/res/values-da/strings.xml
rename to leanback/res/values-da/strings.xml
diff --git a/v17/leanback/res/values-de/strings.xml b/leanback/res/values-de/strings.xml
similarity index 100%
rename from v17/leanback/res/values-de/strings.xml
rename to leanback/res/values-de/strings.xml
diff --git a/v17/leanback/res/values-el/strings.xml b/leanback/res/values-el/strings.xml
similarity index 100%
rename from v17/leanback/res/values-el/strings.xml
rename to leanback/res/values-el/strings.xml
diff --git a/v17/leanback/res/values-en-rAU/strings.xml b/leanback/res/values-en-rAU/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rAU/strings.xml
rename to leanback/res/values-en-rAU/strings.xml
diff --git a/v17/leanback/res/values-en-rCA/strings.xml b/leanback/res/values-en-rCA/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rCA/strings.xml
rename to leanback/res/values-en-rCA/strings.xml
diff --git a/v17/leanback/res/values-en-rGB/strings.xml b/leanback/res/values-en-rGB/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rGB/strings.xml
rename to leanback/res/values-en-rGB/strings.xml
diff --git a/v17/leanback/res/values-en-rIN/strings.xml b/leanback/res/values-en-rIN/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rIN/strings.xml
rename to leanback/res/values-en-rIN/strings.xml
diff --git a/v17/leanback/res/values-en-rXC/strings.xml b/leanback/res/values-en-rXC/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rXC/strings.xml
rename to leanback/res/values-en-rXC/strings.xml
diff --git a/v17/leanback/res/values-es-rUS/strings.xml b/leanback/res/values-es-rUS/strings.xml
similarity index 100%
rename from v17/leanback/res/values-es-rUS/strings.xml
rename to leanback/res/values-es-rUS/strings.xml
diff --git a/v17/leanback/res/values-es/strings.xml b/leanback/res/values-es/strings.xml
similarity index 100%
rename from v17/leanback/res/values-es/strings.xml
rename to leanback/res/values-es/strings.xml
diff --git a/v17/leanback/res/values-et/strings.xml b/leanback/res/values-et/strings.xml
similarity index 100%
rename from v17/leanback/res/values-et/strings.xml
rename to leanback/res/values-et/strings.xml
diff --git a/v17/leanback/res/values-eu/strings.xml b/leanback/res/values-eu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-eu/strings.xml
rename to leanback/res/values-eu/strings.xml
diff --git a/v17/leanback/res/values-fa/strings.xml b/leanback/res/values-fa/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fa/strings.xml
rename to leanback/res/values-fa/strings.xml
diff --git a/v17/leanback/res/values-fi/strings.xml b/leanback/res/values-fi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fi/strings.xml
rename to leanback/res/values-fi/strings.xml
diff --git a/v17/leanback/res/values-fr-rCA/strings.xml b/leanback/res/values-fr-rCA/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fr-rCA/strings.xml
rename to leanback/res/values-fr-rCA/strings.xml
diff --git a/v17/leanback/res/values-fr/strings.xml b/leanback/res/values-fr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fr/strings.xml
rename to leanback/res/values-fr/strings.xml
diff --git a/v17/leanback/res/values-gl/strings.xml b/leanback/res/values-gl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-gl/strings.xml
rename to leanback/res/values-gl/strings.xml
diff --git a/v17/leanback/res/values-gu/strings.xml b/leanback/res/values-gu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-gu/strings.xml
rename to leanback/res/values-gu/strings.xml
diff --git a/v17/leanback/res/values-hi/strings.xml b/leanback/res/values-hi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hi/strings.xml
rename to leanback/res/values-hi/strings.xml
diff --git a/v17/leanback/res/values-hr/strings.xml b/leanback/res/values-hr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hr/strings.xml
rename to leanback/res/values-hr/strings.xml
diff --git a/v17/leanback/res/values-hu/strings.xml b/leanback/res/values-hu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hu/strings.xml
rename to leanback/res/values-hu/strings.xml
diff --git a/v17/leanback/res/values-hy/strings.xml b/leanback/res/values-hy/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hy/strings.xml
rename to leanback/res/values-hy/strings.xml
diff --git a/v17/leanback/res/values-in/strings.xml b/leanback/res/values-in/strings.xml
similarity index 100%
rename from v17/leanback/res/values-in/strings.xml
rename to leanback/res/values-in/strings.xml
diff --git a/v17/leanback/res/values-is/strings.xml b/leanback/res/values-is/strings.xml
similarity index 100%
rename from v17/leanback/res/values-is/strings.xml
rename to leanback/res/values-is/strings.xml
diff --git a/v17/leanback/res/values-it/strings.xml b/leanback/res/values-it/strings.xml
similarity index 100%
rename from v17/leanback/res/values-it/strings.xml
rename to leanback/res/values-it/strings.xml
diff --git a/v17/leanback/res/values-iw/strings.xml b/leanback/res/values-iw/strings.xml
similarity index 100%
rename from v17/leanback/res/values-iw/strings.xml
rename to leanback/res/values-iw/strings.xml
diff --git a/v17/leanback/res/values-ja/strings.xml b/leanback/res/values-ja/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ja/strings.xml
rename to leanback/res/values-ja/strings.xml
diff --git a/v17/leanback/res/values-ka/strings.xml b/leanback/res/values-ka/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ka/strings.xml
rename to leanback/res/values-ka/strings.xml
diff --git a/v17/leanback/res/values-kk/strings.xml b/leanback/res/values-kk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-kk/strings.xml
rename to leanback/res/values-kk/strings.xml
diff --git a/v17/leanback/res/values-km/strings.xml b/leanback/res/values-km/strings.xml
similarity index 100%
rename from v17/leanback/res/values-km/strings.xml
rename to leanback/res/values-km/strings.xml
diff --git a/v17/leanback/res/values-kn/strings.xml b/leanback/res/values-kn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-kn/strings.xml
rename to leanback/res/values-kn/strings.xml
diff --git a/v17/leanback/res/values-ko/strings.xml b/leanback/res/values-ko/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ko/strings.xml
rename to leanback/res/values-ko/strings.xml
diff --git a/v17/leanback/res/values-ky/strings.xml b/leanback/res/values-ky/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ky/strings.xml
rename to leanback/res/values-ky/strings.xml
diff --git a/v17/leanback/res/values-ldrtl/dimens.xml b/leanback/res/values-ldrtl/dimens.xml
similarity index 100%
rename from v17/leanback/res/values-ldrtl/dimens.xml
rename to leanback/res/values-ldrtl/dimens.xml
diff --git a/v17/leanback/res/values-ldrtl/integers.xml b/leanback/res/values-ldrtl/integers.xml
similarity index 100%
rename from v17/leanback/res/values-ldrtl/integers.xml
rename to leanback/res/values-ldrtl/integers.xml
diff --git a/v17/leanback/res/values-lo/strings.xml b/leanback/res/values-lo/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lo/strings.xml
rename to leanback/res/values-lo/strings.xml
diff --git a/v17/leanback/res/values-lt/strings.xml b/leanback/res/values-lt/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lt/strings.xml
rename to leanback/res/values-lt/strings.xml
diff --git a/v17/leanback/res/values-lv/strings.xml b/leanback/res/values-lv/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lv/strings.xml
rename to leanback/res/values-lv/strings.xml
diff --git a/v17/leanback/res/values-mk/strings.xml b/leanback/res/values-mk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mk/strings.xml
rename to leanback/res/values-mk/strings.xml
diff --git a/v17/leanback/res/values-ml/strings.xml b/leanback/res/values-ml/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ml/strings.xml
rename to leanback/res/values-ml/strings.xml
diff --git a/v17/leanback/res/values-mn/strings.xml b/leanback/res/values-mn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mn/strings.xml
rename to leanback/res/values-mn/strings.xml
diff --git a/v17/leanback/res/values-mr/strings.xml b/leanback/res/values-mr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mr/strings.xml
rename to leanback/res/values-mr/strings.xml
diff --git a/v17/leanback/res/values-ms/strings.xml b/leanback/res/values-ms/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ms/strings.xml
rename to leanback/res/values-ms/strings.xml
diff --git a/v17/leanback/res/values-my/strings.xml b/leanback/res/values-my/strings.xml
similarity index 100%
rename from v17/leanback/res/values-my/strings.xml
rename to leanback/res/values-my/strings.xml
diff --git a/v17/leanback/res/values-nb/strings.xml b/leanback/res/values-nb/strings.xml
similarity index 100%
rename from v17/leanback/res/values-nb/strings.xml
rename to leanback/res/values-nb/strings.xml
diff --git a/v17/leanback/res/values-ne/strings.xml b/leanback/res/values-ne/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ne/strings.xml
rename to leanback/res/values-ne/strings.xml
diff --git a/v17/leanback/res/values-nl/strings.xml b/leanback/res/values-nl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-nl/strings.xml
rename to leanback/res/values-nl/strings.xml
diff --git a/v17/leanback/res/values-pa/strings.xml b/leanback/res/values-pa/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pa/strings.xml
rename to leanback/res/values-pa/strings.xml
diff --git a/v17/leanback/res/values-pl/strings.xml b/leanback/res/values-pl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pl/strings.xml
rename to leanback/res/values-pl/strings.xml
diff --git a/v17/leanback/res/values-pt-rBR/strings.xml b/leanback/res/values-pt-rBR/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt-rBR/strings.xml
rename to leanback/res/values-pt-rBR/strings.xml
diff --git a/v17/leanback/res/values-pt-rPT/strings.xml b/leanback/res/values-pt-rPT/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt-rPT/strings.xml
rename to leanback/res/values-pt-rPT/strings.xml
diff --git a/v17/leanback/res/values-pt/strings.xml b/leanback/res/values-pt/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt/strings.xml
rename to leanback/res/values-pt/strings.xml
diff --git a/v17/leanback/res/values-ro/strings.xml b/leanback/res/values-ro/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ro/strings.xml
rename to leanback/res/values-ro/strings.xml
diff --git a/v17/leanback/res/values-ru/strings.xml b/leanback/res/values-ru/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ru/strings.xml
rename to leanback/res/values-ru/strings.xml
diff --git a/v17/leanback/res/values-si/strings.xml b/leanback/res/values-si/strings.xml
similarity index 100%
rename from v17/leanback/res/values-si/strings.xml
rename to leanback/res/values-si/strings.xml
diff --git a/v17/leanback/res/values-sk/strings.xml b/leanback/res/values-sk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sk/strings.xml
rename to leanback/res/values-sk/strings.xml
diff --git a/v17/leanback/res/values-sl/strings.xml b/leanback/res/values-sl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sl/strings.xml
rename to leanback/res/values-sl/strings.xml
diff --git a/v17/leanback/res/values-sq/strings.xml b/leanback/res/values-sq/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sq/strings.xml
rename to leanback/res/values-sq/strings.xml
diff --git a/v17/leanback/res/values-sr/strings.xml b/leanback/res/values-sr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sr/strings.xml
rename to leanback/res/values-sr/strings.xml
diff --git a/v17/leanback/res/values-sv/strings.xml b/leanback/res/values-sv/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sv/strings.xml
rename to leanback/res/values-sv/strings.xml
diff --git a/v17/leanback/res/values-sw/strings.xml b/leanback/res/values-sw/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sw/strings.xml
rename to leanback/res/values-sw/strings.xml
diff --git a/v17/leanback/res/values-ta/strings.xml b/leanback/res/values-ta/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ta/strings.xml
rename to leanback/res/values-ta/strings.xml
diff --git a/v17/leanback/res/values-te/strings.xml b/leanback/res/values-te/strings.xml
similarity index 100%
rename from v17/leanback/res/values-te/strings.xml
rename to leanback/res/values-te/strings.xml
diff --git a/v17/leanback/res/values-th/strings.xml b/leanback/res/values-th/strings.xml
similarity index 100%
rename from v17/leanback/res/values-th/strings.xml
rename to leanback/res/values-th/strings.xml
diff --git a/v17/leanback/res/values-tl/strings.xml b/leanback/res/values-tl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-tl/strings.xml
rename to leanback/res/values-tl/strings.xml
diff --git a/v17/leanback/res/values-tr/strings.xml b/leanback/res/values-tr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-tr/strings.xml
rename to leanback/res/values-tr/strings.xml
diff --git a/v17/leanback/res/values-uk/strings.xml b/leanback/res/values-uk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-uk/strings.xml
rename to leanback/res/values-uk/strings.xml
diff --git a/v17/leanback/res/values-ur/strings.xml b/leanback/res/values-ur/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ur/strings.xml
rename to leanback/res/values-ur/strings.xml
diff --git a/v17/leanback/res/values-uz/strings.xml b/leanback/res/values-uz/strings.xml
similarity index 100%
rename from v17/leanback/res/values-uz/strings.xml
rename to leanback/res/values-uz/strings.xml
diff --git a/v17/leanback/res/values-v18/themes.xml b/leanback/res/values-v18/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v18/themes.xml
rename to leanback/res/values-v18/themes.xml
diff --git a/v17/leanback/res/values-v19/themes.xml b/leanback/res/values-v19/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v19/themes.xml
rename to leanback/res/values-v19/themes.xml
diff --git a/v17/leanback/res/values-v21/styles.xml b/leanback/res/values-v21/styles.xml
similarity index 100%
rename from v17/leanback/res/values-v21/styles.xml
rename to leanback/res/values-v21/styles.xml
diff --git a/v17/leanback/res/values-v21/themes.xml b/leanback/res/values-v21/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v21/themes.xml
rename to leanback/res/values-v21/themes.xml
diff --git a/v17/leanback/res/values-v22/integers.xml b/leanback/res/values-v22/integers.xml
similarity index 100%
rename from v17/leanback/res/values-v22/integers.xml
rename to leanback/res/values-v22/integers.xml
diff --git a/v17/leanback/res/values-vi/strings.xml b/leanback/res/values-vi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-vi/strings.xml
rename to leanback/res/values-vi/strings.xml
diff --git a/v17/leanback/res/values-zh-rCN/strings.xml b/leanback/res/values-zh-rCN/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rCN/strings.xml
rename to leanback/res/values-zh-rCN/strings.xml
diff --git a/v17/leanback/res/values-zh-rHK/strings.xml b/leanback/res/values-zh-rHK/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rHK/strings.xml
rename to leanback/res/values-zh-rHK/strings.xml
diff --git a/v17/leanback/res/values-zh-rTW/strings.xml b/leanback/res/values-zh-rTW/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rTW/strings.xml
rename to leanback/res/values-zh-rTW/strings.xml
diff --git a/v17/leanback/res/values-zu/strings.xml b/leanback/res/values-zu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zu/strings.xml
rename to leanback/res/values-zu/strings.xml
diff --git a/v17/leanback/res/values/attrs.xml b/leanback/res/values/attrs.xml
similarity index 100%
rename from v17/leanback/res/values/attrs.xml
rename to leanback/res/values/attrs.xml
diff --git a/v17/leanback/res/values/colors.xml b/leanback/res/values/colors.xml
similarity index 100%
rename from v17/leanback/res/values/colors.xml
rename to leanback/res/values/colors.xml
diff --git a/v17/leanback/res/values/dimens.xml b/leanback/res/values/dimens.xml
similarity index 100%
rename from v17/leanback/res/values/dimens.xml
rename to leanback/res/values/dimens.xml
diff --git a/v17/leanback/res/values/ids.xml b/leanback/res/values/ids.xml
similarity index 100%
rename from v17/leanback/res/values/ids.xml
rename to leanback/res/values/ids.xml
diff --git a/v17/leanback/res/values/integers.xml b/leanback/res/values/integers.xml
similarity index 100%
rename from v17/leanback/res/values/integers.xml
rename to leanback/res/values/integers.xml
diff --git a/v17/leanback/res/values/strings.xml b/leanback/res/values/strings.xml
similarity index 100%
rename from v17/leanback/res/values/strings.xml
rename to leanback/res/values/strings.xml
diff --git a/v17/leanback/res/values/styles.xml b/leanback/res/values/styles.xml
similarity index 100%
rename from v17/leanback/res/values/styles.xml
rename to leanback/res/values/styles.xml
diff --git a/v17/leanback/res/values/themes.xml b/leanback/res/values/themes.xml
similarity index 100%
rename from v17/leanback/res/values/themes.xml
rename to leanback/res/values/themes.xml
diff --git a/v17/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java b/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
rename to leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java b/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
rename to leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java b/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
rename to leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
rename to leanback/src/android/support/v17/leanback/app/BackgroundManager.java
diff --git a/leanback/src/android/support/v17/leanback/app/BaseFragment.java b/leanback/src/android/support/v17/leanback/app/BaseFragment.java
new file mode 100644
index 0000000..ea46011
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BaseFragment.java
@@ -0,0 +1,323 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BaseSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.util.StateMachine.Condition;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+/**
+ * Base class for leanback Fragments. This class is not intended to be subclassed by apps.
+ * @deprecated use {@link BaseSupportFragment}
+ */
+@Deprecated
+@SuppressWarnings("FragmentNotInstantiable")
+public class BaseFragment extends BrandedFragment {
+
+ /**
+ * The start state for all
+ */
+ final State STATE_START = new State("START", true, false);
+
+ /**
+ * Initial State for ENTRNACE transition.
+ */
+ final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
+
+ /**
+ * prepareEntranceTransition is just called, but view not ready yet. We can enable the
+ * busy spinner.
+ */
+ final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
+ @Override
+ public void run() {
+ mProgressBarManager.show();
+ }
+ };
+
+ /**
+ * prepareEntranceTransition is called and main content view to slide in was created, so we can
+ * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
+ * in this State, the process is very different in subclass, e.g. BrowseFragment hide header
+ * views and hide main fragment view in two steps.
+ */
+ final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
+ "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
+ @Override
+ public void run() {
+ onEntranceTransitionPrepare();
+ }
+ };
+
+ /**
+ * execute the entrance transition.
+ */
+ final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
+ @Override
+ public void run() {
+ mProgressBarManager.hide();
+ onExecuteEntranceTransition();
+ }
+ };
+
+ /**
+ * execute onEntranceTransitionEnd.
+ */
+ final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
+ @Override
+ public void run() {
+ onEntranceTransitionEnd();
+ }
+ };
+
+ /**
+ * either entrance transition completed or skipped
+ */
+ final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
+
+ /**
+ * Event fragment.onCreate()
+ */
+ final Event EVT_ON_CREATE = new Event("onCreate");
+
+ /**
+ * Event fragment.onViewCreated()
+ */
+ final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
+
+ /**
+ * Event for {@link #prepareEntranceTransition()} is called.
+ */
+ final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
+
+ /**
+ * Event for {@link #startEntranceTransition()} is called.
+ */
+ final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
+
+ /**
+ * Event for entrance transition is ended through Transition listener.
+ */
+ final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
+
+ /**
+ * Event for skipping entrance transition if not supported.
+ */
+ final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
+ @Override
+ public boolean canProceed() {
+ return !TransitionHelper.systemSupportsEntranceTransitions();
+ }
+ };
+
+ final StateMachine mStateMachine = new StateMachine();
+
+ Object mEntranceTransition;
+ final ProgressBarManager mProgressBarManager = new ProgressBarManager();
+
+ @SuppressLint("ValidFragment")
+ BaseFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ createStateMachineStates();
+ createStateMachineTransitions();
+ mStateMachine.start();
+ super.onCreate(savedInstanceState);
+ mStateMachine.fireEvent(EVT_ON_CREATE);
+ }
+
+ void createStateMachineStates() {
+ mStateMachine.addState(STATE_START);
+ mStateMachine.addState(STATE_ENTRANCE_INIT);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
+ mStateMachine.addState(STATE_ENTRANCE_PERFORM);
+ mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
+ mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
+ }
+
+ void createStateMachineTransitions() {
+ mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
+ EVT_PREPARE_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_PERFORM,
+ EVT_START_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ STATE_ENTRANCE_PERFORM);
+ mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
+ STATE_ENTRANCE_ON_ENDED,
+ EVT_ENTRANCE_END);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
+ }
+
+ /**
+ * Enables entrance transition.<p>
+ * Entrance transition is the standard slide-in transition that shows rows of data in
+ * browse screen and details screen.
+ * <p>
+ * The method is ignored before LOLLIPOP (API21).
+ * <p>
+ * This method must be called in or
+ * before onCreate(). Typically entrance transition should be enabled when savedInstance is
+ * null so that fragment restored from instanceState does not run an extra entrance transition.
+ * When the entrance transition is enabled, the fragment will make headers and content
+ * hidden initially.
+ * When data of rows are ready, app must call {@link #startEntranceTransition()} to kick off
+ * the transition, otherwise the rows will be invisible forever.
+ * <p>
+ * It is similar to android:windowsEnterTransition and can be considered a late-executed
+ * android:windowsEnterTransition controlled by app. There are two reasons that app needs it:
+ * <li> Workaround the problem that activity transition is not available between launcher and
+ * app. Browse activity must programmatically start the slide-in transition.</li>
+ * <li> Separates DetailsOverviewRow transition from other rows transition. So that
+ * the DetailsOverviewRow transition can be executed earlier without waiting for all rows
+ * to be loaded.</li>
+ * <p>
+ * Transition object is returned by createEntranceTransition(). Typically the app does not need
+ * override the default transition that browse and details provides.
+ */
+ public void prepareEntranceTransition() {
+ mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
+ }
+
+ /**
+ * Create entrance transition. Subclass can override to load transition from
+ * resource or construct manually. Typically app does not need to
+ * override the default transition that browse and details provides.
+ */
+ protected Object createEntranceTransition() {
+ return null;
+ }
+
+ /**
+ * Run entrance transition. Subclass may use TransitionManager to perform
+ * go(Scene) or beginDelayedTransition(). App should not override the default
+ * implementation of browse and details fragment.
+ */
+ protected void runEntranceTransition(Object entranceTransition) {
+ }
+
+ /**
+ * Callback when entrance transition is prepared. This is when fragment should
+ * stop user input and animations.
+ */
+ protected void onEntranceTransitionPrepare() {
+ }
+
+ /**
+ * Callback when entrance transition is started. This is when fragment should
+ * stop processing layout.
+ */
+ protected void onEntranceTransitionStart() {
+ }
+
+ /**
+ * Callback when entrance transition is ended.
+ */
+ protected void onEntranceTransitionEnd() {
+ }
+
+ /**
+ * When fragment finishes loading data, it should call startEntranceTransition()
+ * to execute the entrance transition.
+ * startEntranceTransition() will start transition only if both two conditions
+ * are satisfied:
+ * <li> prepareEntranceTransition() was called.</li>
+ * <li> has not executed entrance transition yet.</li>
+ * <p>
+ * If startEntranceTransition() is called before onViewCreated(), it will be pending
+ * and executed when view is created.
+ */
+ public void startEntranceTransition() {
+ mStateMachine.fireEvent(EVT_START_ENTRANCE);
+ }
+
+ void onExecuteEntranceTransition() {
+ // wait till views get their initial position before start transition
+ final View view = getView();
+ if (view == null) {
+ // fragment view destroyed, transition not needed
+ return;
+ }
+ view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ view.getViewTreeObserver().removeOnPreDrawListener(this);
+ if (FragmentUtil.getContext(BaseFragment.this) == null || getView() == null) {
+ // bail out if fragment is destroyed immediately after startEntranceTransition
+ return true;
+ }
+ internalCreateEntranceTransition();
+ onEntranceTransitionStart();
+ if (mEntranceTransition != null) {
+ runEntranceTransition(mEntranceTransition);
+ } else {
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
+ }
+ return false;
+ }
+ });
+ view.invalidate();
+ }
+
+ void internalCreateEntranceTransition() {
+ mEntranceTransition = createEntranceTransition();
+ if (mEntranceTransition == null) {
+ return;
+ }
+ TransitionHelper.addTransitionListener(mEntranceTransition, new TransitionListener() {
+ @Override
+ public void onTransitionEnd(Object transition) {
+ mEntranceTransition = null;
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
+ }
+ });
+ }
+
+ /**
+ * Returns the {@link ProgressBarManager}.
+ * @return The {@link ProgressBarManager}.
+ */
+ public final ProgressBarManager getProgressBarManager() {
+ return mProgressBarManager;
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
new file mode 100644
index 0000000..97a5b84
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -0,0 +1,308 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BaseRowSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An internal base class for a fragment containing a list of rows.
+ * @deprecated use {@link BaseRowSupportFragment}
+ */
+@Deprecated
+abstract class BaseRowFragment extends Fragment {
+ private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+ private ObjectAdapter mAdapter;
+ VerticalGridView mVerticalGridView;
+ private PresenterSelector mPresenterSelector;
+ final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
+ int mSelectedPosition = -1;
+ private boolean mPendingTransitionPrepare;
+ private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
+
+ abstract int getLayoutResourceId();
+
+ private final OnChildViewHolderSelectedListener mRowSelectedListener =
+ new OnChildViewHolderSelectedListener() {
+ @Override
+ public void onChildViewHolderSelected(RecyclerView parent,
+ RecyclerView.ViewHolder view, int position, int subposition) {
+ if (!mLateSelectionObserver.mIsLateSelection) {
+ mSelectedPosition = position;
+ onRowSelected(parent, view, position, subposition);
+ }
+ }
+ };
+
+ void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,
+ int position, int subposition) {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(getLayoutResourceId(), container, false);
+ mVerticalGridView = findGridViewFromRoot(view);
+ if (mPendingTransitionPrepare) {
+ mPendingTransitionPrepare = false;
+ onTransitionPrepare();
+ }
+ return view;
+ }
+
+ VerticalGridView findGridViewFromRoot(View view) {
+ return (VerticalGridView) view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
+ }
+ setAdapterAndSelection();
+ mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
+ }
+
+ /**
+ * This class waits for the adapter to be updated before setting the selected
+ * row.
+ */
+ private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {
+ boolean mIsLateSelection = false;
+
+ LateSelectionObserver() {
+ }
+
+ @Override
+ public void onChanged() {
+ performLateSelection();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ performLateSelection();
+ }
+
+ void startLateSelection() {
+ mIsLateSelection = true;
+ mBridgeAdapter.registerAdapterDataObserver(this);
+ }
+
+ void performLateSelection() {
+ clear();
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+
+ void clear() {
+ if (mIsLateSelection) {
+ mIsLateSelection = false;
+ mBridgeAdapter.unregisterAdapterDataObserver(this);
+ }
+ }
+ }
+
+ void setAdapterAndSelection() {
+ if (mAdapter == null) {
+ // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter
+ // to RecyclerView, it will not be allowed to change "hasStableId" to true.
+ return;
+ }
+ if (mVerticalGridView.getAdapter() != mBridgeAdapter) {
+ // avoid extra layout if ItemBridgeAdapter was already set.
+ mVerticalGridView.setAdapter(mBridgeAdapter);
+ }
+ // We don't set the selected position unless we've data in the adapter.
+ boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;
+ if (lateSelection) {
+ mLateSelectionObserver.startLateSelection();
+ } else if (mSelectedPosition >= 0) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mLateSelectionObserver.clear();
+ mVerticalGridView = null;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
+ }
+
+ /**
+ * Set the presenter selector used to create and bind views.
+ */
+ public final void setPresenterSelector(PresenterSelector presenterSelector) {
+ if (mPresenterSelector != presenterSelector) {
+ mPresenterSelector = presenterSelector;
+ updateAdapter();
+ }
+ }
+
+ /**
+ * Get the presenter selector used to create and bind views.
+ */
+ public final PresenterSelector getPresenterSelector() {
+ return mPresenterSelector;
+ }
+
+ /**
+ * Sets the adapter that represents a list of rows.
+ * @param rowsAdapter Adapter that represents list of rows.
+ */
+ public final void setAdapter(ObjectAdapter rowsAdapter) {
+ if (mAdapter != rowsAdapter) {
+ mAdapter = rowsAdapter;
+ updateAdapter();
+ }
+ }
+
+ /**
+ * Returns the Adapter that represents list of rows.
+ * @return Adapter that represents list of rows.
+ */
+ public final ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.
+ * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.
+ */
+ public final ItemBridgeAdapter getBridgeAdapter() {
+ return mBridgeAdapter;
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Gets position of currently selected row.
+ * @return Position of currently selected row.
+ */
+ public int getSelectedPosition() {
+ return mSelectedPosition;
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ if (mSelectedPosition == position) {
+ return;
+ }
+ mSelectedPosition = position;
+ if (mVerticalGridView != null) {
+ if (mLateSelectionObserver.mIsLateSelection) {
+ return;
+ }
+ if (smooth) {
+ mVerticalGridView.setSelectedPositionSmooth(position);
+ } else {
+ mVerticalGridView.setSelectedPosition(position);
+ }
+ }
+ }
+
+ public final VerticalGridView getVerticalGridView() {
+ return mVerticalGridView;
+ }
+
+ void updateAdapter() {
+ mBridgeAdapter.setAdapter(mAdapter);
+ mBridgeAdapter.setPresenter(mPresenterSelector);
+
+ if (mVerticalGridView != null) {
+ setAdapterAndSelection();
+ }
+ }
+
+ Object getItem(Row row, int position) {
+ if (row instanceof ListRow) {
+ return ((ListRow) row).getAdapter().get(position);
+ } else {
+ return null;
+ }
+ }
+
+ public boolean onTransitionPrepare() {
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setAnimateChildLayout(false);
+ mVerticalGridView.setScrollEnabled(false);
+ return true;
+ }
+ mPendingTransitionPrepare = true;
+ return false;
+ }
+
+ public void onTransitionStart() {
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setPruneChild(false);
+ mVerticalGridView.setLayoutFrozen(true);
+ mVerticalGridView.setFocusSearchDisabled(true);
+ }
+ }
+
+ public void onTransitionEnd() {
+ // be careful that fragment might be destroyed before header transition ends.
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setLayoutFrozen(false);
+ mVerticalGridView.setAnimateChildLayout(true);
+ mVerticalGridView.setPruneChild(true);
+ mVerticalGridView.setFocusSearchDisabled(false);
+ mVerticalGridView.setScrollEnabled(true);
+ }
+ }
+
+ public void setAlignment(int windowAlignOffsetTop) {
+ if (mVerticalGridView != null) {
+ // align the top edge of item
+ mVerticalGridView.setItemAlignmentOffset(0);
+ mVerticalGridView.setItemAlignmentOffsetPercent(
+ VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+
+ // align to a fixed position from top
+ mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);
+ mVerticalGridView.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
new file mode 100644
index 0000000..6a477ab
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An internal base class for a fragment containing a list of rows.
+ */
+abstract class BaseRowSupportFragment extends Fragment {
+ private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+ private ObjectAdapter mAdapter;
+ VerticalGridView mVerticalGridView;
+ private PresenterSelector mPresenterSelector;
+ final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
+ int mSelectedPosition = -1;
+ private boolean mPendingTransitionPrepare;
+ private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
+
+ abstract int getLayoutResourceId();
+
+ private final OnChildViewHolderSelectedListener mRowSelectedListener =
+ new OnChildViewHolderSelectedListener() {
+ @Override
+ public void onChildViewHolderSelected(RecyclerView parent,
+ RecyclerView.ViewHolder view, int position, int subposition) {
+ if (!mLateSelectionObserver.mIsLateSelection) {
+ mSelectedPosition = position;
+ onRowSelected(parent, view, position, subposition);
+ }
+ }
+ };
+
+ void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,
+ int position, int subposition) {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(getLayoutResourceId(), container, false);
+ mVerticalGridView = findGridViewFromRoot(view);
+ if (mPendingTransitionPrepare) {
+ mPendingTransitionPrepare = false;
+ onTransitionPrepare();
+ }
+ return view;
+ }
+
+ VerticalGridView findGridViewFromRoot(View view) {
+ return (VerticalGridView) view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
+ }
+ setAdapterAndSelection();
+ mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
+ }
+
+ /**
+ * This class waits for the adapter to be updated before setting the selected
+ * row.
+ */
+ private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {
+ boolean mIsLateSelection = false;
+
+ LateSelectionObserver() {
+ }
+
+ @Override
+ public void onChanged() {
+ performLateSelection();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ performLateSelection();
+ }
+
+ void startLateSelection() {
+ mIsLateSelection = true;
+ mBridgeAdapter.registerAdapterDataObserver(this);
+ }
+
+ void performLateSelection() {
+ clear();
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+
+ void clear() {
+ if (mIsLateSelection) {
+ mIsLateSelection = false;
+ mBridgeAdapter.unregisterAdapterDataObserver(this);
+ }
+ }
+ }
+
+ void setAdapterAndSelection() {
+ if (mAdapter == null) {
+ // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter
+ // to RecyclerView, it will not be allowed to change "hasStableId" to true.
+ return;
+ }
+ if (mVerticalGridView.getAdapter() != mBridgeAdapter) {
+ // avoid extra layout if ItemBridgeAdapter was already set.
+ mVerticalGridView.setAdapter(mBridgeAdapter);
+ }
+ // We don't set the selected position unless we've data in the adapter.
+ boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;
+ if (lateSelection) {
+ mLateSelectionObserver.startLateSelection();
+ } else if (mSelectedPosition >= 0) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mLateSelectionObserver.clear();
+ mVerticalGridView = null;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
+ }
+
+ /**
+ * Set the presenter selector used to create and bind views.
+ */
+ public final void setPresenterSelector(PresenterSelector presenterSelector) {
+ if (mPresenterSelector != presenterSelector) {
+ mPresenterSelector = presenterSelector;
+ updateAdapter();
+ }
+ }
+
+ /**
+ * Get the presenter selector used to create and bind views.
+ */
+ public final PresenterSelector getPresenterSelector() {
+ return mPresenterSelector;
+ }
+
+ /**
+ * Sets the adapter that represents a list of rows.
+ * @param rowsAdapter Adapter that represents list of rows.
+ */
+ public final void setAdapter(ObjectAdapter rowsAdapter) {
+ if (mAdapter != rowsAdapter) {
+ mAdapter = rowsAdapter;
+ updateAdapter();
+ }
+ }
+
+ /**
+ * Returns the Adapter that represents list of rows.
+ * @return Adapter that represents list of rows.
+ */
+ public final ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.
+ * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.
+ */
+ public final ItemBridgeAdapter getBridgeAdapter() {
+ return mBridgeAdapter;
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Gets position of currently selected row.
+ * @return Position of currently selected row.
+ */
+ public int getSelectedPosition() {
+ return mSelectedPosition;
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ if (mSelectedPosition == position) {
+ return;
+ }
+ mSelectedPosition = position;
+ if (mVerticalGridView != null) {
+ if (mLateSelectionObserver.mIsLateSelection) {
+ return;
+ }
+ if (smooth) {
+ mVerticalGridView.setSelectedPositionSmooth(position);
+ } else {
+ mVerticalGridView.setSelectedPosition(position);
+ }
+ }
+ }
+
+ public final VerticalGridView getVerticalGridView() {
+ return mVerticalGridView;
+ }
+
+ void updateAdapter() {
+ mBridgeAdapter.setAdapter(mAdapter);
+ mBridgeAdapter.setPresenter(mPresenterSelector);
+
+ if (mVerticalGridView != null) {
+ setAdapterAndSelection();
+ }
+ }
+
+ Object getItem(Row row, int position) {
+ if (row instanceof ListRow) {
+ return ((ListRow) row).getAdapter().get(position);
+ } else {
+ return null;
+ }
+ }
+
+ public boolean onTransitionPrepare() {
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setAnimateChildLayout(false);
+ mVerticalGridView.setScrollEnabled(false);
+ return true;
+ }
+ mPendingTransitionPrepare = true;
+ return false;
+ }
+
+ public void onTransitionStart() {
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setPruneChild(false);
+ mVerticalGridView.setLayoutFrozen(true);
+ mVerticalGridView.setFocusSearchDisabled(true);
+ }
+ }
+
+ public void onTransitionEnd() {
+ // be careful that fragment might be destroyed before header transition ends.
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setLayoutFrozen(false);
+ mVerticalGridView.setAnimateChildLayout(true);
+ mVerticalGridView.setPruneChild(true);
+ mVerticalGridView.setFocusSearchDisabled(false);
+ mVerticalGridView.setScrollEnabled(true);
+ }
+ }
+
+ public void setAlignment(int windowAlignOffsetTop) {
+ if (mVerticalGridView != null) {
+ // align the top edge of item
+ mVerticalGridView.setItemAlignmentOffset(0);
+ mVerticalGridView.setItemAlignmentOffsetPercent(
+ VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+
+ // align to a fixed position from top
+ mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);
+ mVerticalGridView.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/BrandedFragment.java b/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
new file mode 100644
index 0000000..415c13e
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
@@ -0,0 +1,340 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrandedSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.SearchOrbView;
+import android.support.v17.leanback.widget.TitleHelper;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.app.Fragment;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Fragment class for managing search and branding using a view that implements
+ * {@link TitleViewAdapter.Provider}.
+ * @deprecated use {@link BrandedSupportFragment}
+ */
+@Deprecated
+public class BrandedFragment extends Fragment {
+
+ // BUNDLE attribute for title is showing
+ private static final String TITLE_SHOW = "titleShow";
+
+ private boolean mShowingTitle = true;
+ private CharSequence mTitle;
+ private Drawable mBadgeDrawable;
+ private View mTitleView;
+ private TitleViewAdapter mTitleViewAdapter;
+ private SearchOrbView.Colors mSearchAffordanceColors;
+ private boolean mSearchAffordanceColorSet;
+ private View.OnClickListener mExternalOnSearchClickedListener;
+ private TitleHelper mTitleHelper;
+
+ /**
+ * Called by {@link #installTitleView(LayoutInflater, ViewGroup, Bundle)} to inflate
+ * title view. Default implementation uses layout file lb_browse_title.
+ * Subclass may override and use its own layout, the layout must have a descendant with id
+ * browse_title_group that implements {@link TitleViewAdapter.Provider}. Subclass may return
+ * null if no title is needed.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate
+ * any views in the fragment,
+ * @param parent Parent of title view.
+ * @param savedInstanceState If non-null, this fragment is being re-constructed
+ * from a previous saved state as given here.
+ * @return Title view which must have a descendant with id browse_title_group that implements
+ * {@link TitleViewAdapter.Provider}, or null for no title view.
+ */
+ public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ TypedValue typedValue = new TypedValue();
+ boolean found = parent.getContext().getTheme().resolveAttribute(
+ R.attr.browseTitleViewLayout, typedValue, true);
+ return inflater.inflate(found ? typedValue.resourceId : R.layout.lb_browse_title,
+ parent, false);
+ }
+
+ /**
+ * Inflate title view and add to parent. This method should be called in
+ * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ * @param inflater The LayoutInflater object that can be used to inflate
+ * any views in the fragment,
+ * @param parent Parent of title view.
+ * @param savedInstanceState If non-null, this fragment is being re-constructed
+ * from a previous saved state as given here.
+ */
+ public void installTitleView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ View titleLayoutRoot = onInflateTitleView(inflater, parent, savedInstanceState);
+ if (titleLayoutRoot != null) {
+ parent.addView(titleLayoutRoot);
+ setTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group));
+ } else {
+ setTitleView(null);
+ }
+ }
+
+ /**
+ * Sets the view that implemented {@link TitleViewAdapter}.
+ * @param titleView The view that implemented {@link TitleViewAdapter.Provider}.
+ */
+ public void setTitleView(View titleView) {
+ mTitleView = titleView;
+ if (mTitleView == null) {
+ mTitleViewAdapter = null;
+ mTitleHelper = null;
+ } else {
+ mTitleViewAdapter = ((TitleViewAdapter.Provider) mTitleView).getTitleViewAdapter();
+ mTitleViewAdapter.setTitle(mTitle);
+ mTitleViewAdapter.setBadgeDrawable(mBadgeDrawable);
+ if (mSearchAffordanceColorSet) {
+ mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
+ if (mExternalOnSearchClickedListener != null) {
+ setOnSearchClickedListener(mExternalOnSearchClickedListener);
+ }
+ if (getView() instanceof ViewGroup) {
+ mTitleHelper = new TitleHelper((ViewGroup) getView(), mTitleView);
+ }
+ }
+ }
+
+ /**
+ * Returns the view that implements {@link TitleViewAdapter.Provider}.
+ * @return The view that implements {@link TitleViewAdapter.Provider}.
+ */
+ public View getTitleView() {
+ return mTitleView;
+ }
+
+ /**
+ * Returns the {@link TitleViewAdapter} implemented by title view.
+ * @return The {@link TitleViewAdapter} implemented by title view.
+ */
+ public TitleViewAdapter getTitleViewAdapter() {
+ return mTitleViewAdapter;
+ }
+
+ /**
+ * Returns the {@link TitleHelper}.
+ */
+ TitleHelper getTitleHelper() {
+ return mTitleHelper;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(TITLE_SHOW, mShowingTitle);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (savedInstanceState != null) {
+ mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);
+ }
+ if (mTitleView != null && view instanceof ViewGroup) {
+ mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
+ mTitleHelper.showTitle(mShowingTitle);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mTitleHelper = null;
+ }
+
+ /**
+ * Shows or hides the title view.
+ * @param show True to show title view, false to hide title view.
+ */
+ public void showTitle(boolean show) {
+ // TODO: handle interruptions?
+ if (show == mShowingTitle) {
+ return;
+ }
+ mShowingTitle = show;
+ if (mTitleHelper != null) {
+ mTitleHelper.showTitle(show);
+ }
+ }
+
+ /**
+ * Changes title view's components visibility and shows title.
+ * @param flags Flags representing the visibility of components inside title view.
+ * @see TitleViewAdapter#SEARCH_VIEW_VISIBLE
+ * @see TitleViewAdapter#BRANDING_VIEW_VISIBLE
+ * @see TitleViewAdapter#FULL_VIEW_VISIBLE
+ * @see TitleViewAdapter#updateComponentsVisibility(int)
+ */
+ public void showTitle(int flags) {
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.updateComponentsVisibility(flags);
+ }
+ showTitle(true);
+ }
+
+ /**
+ * Sets the drawable displayed in the fragment title.
+ *
+ * @param drawable The Drawable to display in the fragment title.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ if (mBadgeDrawable != drawable) {
+ mBadgeDrawable = drawable;
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setBadgeDrawable(drawable);
+ }
+ }
+ }
+
+ /**
+ * Returns the badge drawable used in the fragment title.
+ * @return The badge drawable used in the fragment title.
+ */
+ public Drawable getBadgeDrawable() {
+ return mBadgeDrawable;
+ }
+
+ /**
+ * Sets title text for the fragment.
+ *
+ * @param title The title text of the fragment.
+ */
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setTitle(title);
+ }
+ }
+
+ /**
+ * Returns the title text for the fragment.
+ * @return Title text for the fragment.
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets a click listener for the search affordance.
+ *
+ * <p>The presence of a listener will change the visibility of the search
+ * affordance in the fragment title. When set to non-null, the title will
+ * contain an element that a user may click to begin a search.
+ *
+ * <p>The listener's {@link View.OnClickListener#onClick onClick} method
+ * will be invoked when the user clicks on the search element.
+ *
+ * @param listener The listener to call when the search element is clicked.
+ */
+ public void setOnSearchClickedListener(View.OnClickListener listener) {
+ mExternalOnSearchClickedListener = listener;
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setOnSearchClickedListener(listener);
+ }
+ }
+
+ /**
+ * Sets the {@link android.support.v17.leanback.widget.SearchOrbView.Colors} used to draw the
+ * search affordance.
+ *
+ * @param colors Colors used to draw search affordance.
+ */
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ mSearchAffordanceColors = colors;
+ mSearchAffordanceColorSet = true;
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
+ }
+
+ /**
+ * Returns the {@link android.support.v17.leanback.widget.SearchOrbView.Colors}
+ * used to draw the search affordance.
+ */
+ public SearchOrbView.Colors getSearchAffordanceColors() {
+ if (mSearchAffordanceColorSet) {
+ return mSearchAffordanceColors;
+ }
+ if (mTitleViewAdapter == null) {
+ throw new IllegalStateException("Fragment views not yet created");
+ }
+ return mTitleViewAdapter.getSearchAffordanceColors();
+ }
+
+ /**
+ * Sets the color used to draw the search affordance.
+ * A default brighter color will be set by the framework.
+ *
+ * @param color The color to use for the search affordance.
+ */
+ public void setSearchAffordanceColor(int color) {
+ setSearchAffordanceColors(new SearchOrbView.Colors(color));
+ }
+
+ /**
+ * Returns the color used to draw the search affordance.
+ */
+ public int getSearchAffordanceColor() {
+ return getSearchAffordanceColors().color;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mTitleViewAdapter != null) {
+ showTitle(mShowingTitle);
+ mTitleViewAdapter.setAnimationEnabled(true);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setAnimationEnabled(false);
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mTitleViewAdapter != null) {
+ mTitleViewAdapter.setAnimationEnabled(true);
+ }
+ }
+
+ /**
+ * Returns true/false to indicate the visibility of TitleView.
+ *
+ * @return boolean to indicate whether or not it's showing the title.
+ */
+ public final boolean isShowingTitle() {
+ return mShowingTitle;
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
new file mode 100644
index 0000000..c561ea9
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -0,0 +1,1868 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
+import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.InvisibleRowPresenter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowHeaderPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.ScaleFrameLayout;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
+import android.app.FragmentTransaction;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewTreeObserver;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A fragment for creating Leanback browse screens. It is composed of a
+ * RowsFragment and a HeadersFragment.
+ * <p>
+ * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
+ * of rows in a vertical list. The elements in this adapter must be subclasses
+ * of {@link Row}.
+ * <p>
+ * The HeadersFragment can be set to be either shown or hidden by default, or
+ * may be disabled entirely. See {@link #setHeadersState} for details.
+ * <p>
+ * By default the BrowseFragment includes support for returning to the headers
+ * when the user presses Back. For Activities that customize {@link
+ * android.app.Activity#onBackPressed()}, you must disable this default Back key support by
+ * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
+ * use {@link BrowseFragment.BrowseTransitionListener} and
+ * {@link #startHeadersTransition(boolean)}.
+ * <p>
+ * The recommended theme to use with a BrowseFragment is
+ * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
+ * </p>
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+@Deprecated
+public class BrowseFragment extends BaseFragment {
+
+ // BUNDLE attribute for saving header show/hide status when backstack is used:
+ static final String HEADER_STACK_INDEX = "headerStackIndex";
+ // BUNDLE attribute for saving header show/hide status when backstack is not used:
+ static final String HEADER_SHOW = "headerShow";
+ private static final String IS_PAGE_ROW = "isPageRow";
+ private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+
+ /**
+ * State to hide headers fragment.
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionStartState();
+ }
+ };
+
+ /**
+ * Event for Header fragment view is created, we could perform
+ * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+ */
+ final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+ /**
+ * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+ * {@link #onEntranceTransitionPrepare()}.
+ */
+ final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+ /**
+ * Event that data for the screen is ready, this is additional requirement to launch entrance
+ * transition.
+ */
+ final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ // when headers fragment view is created we could setEntranceTransitionStartState()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+ EVT_HEADER_VIEW_CREATED);
+
+ // add additional requirement for onEntranceTransitionPrepare()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ // add additional requirement to launch entrance transition.
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
+ EVT_SCREEN_DATA_READY);
+ }
+
+ final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
+ int mLastEntryCount;
+ int mIndexOfHeadersBackStack;
+
+ BackStackListener() {
+ mLastEntryCount = getFragmentManager().getBackStackEntryCount();
+ mIndexOfHeadersBackStack = -1;
+ }
+
+ void load(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
+ mShowingHeaders = mIndexOfHeadersBackStack == -1;
+ } else {
+ if (!mShowingHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ }
+ }
+ }
+
+ void save(Bundle outState) {
+ outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
+ }
+
+
+ @Override
+ public void onBackStackChanged() {
+ if (getFragmentManager() == null) {
+ Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
+ return;
+ }
+ int count = getFragmentManager().getBackStackEntryCount();
+ // if backstack is growing and last pushed entry is "headers" backstack,
+ // remember the index of the entry.
+ if (count > mLastEntryCount) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
+ if (mWithHeadersBackStackName.equals(entry.getName())) {
+ mIndexOfHeadersBackStack = count - 1;
+ }
+ } else if (count < mLastEntryCount) {
+ // if popped "headers" backstack, initiate the show header transition if needed
+ if (mIndexOfHeadersBackStack >= count) {
+ if (!isHeadersDataReady()) {
+ // if main fragment was restored first before BrowseFragment's adapter gets
+ // restored: don't start header transition, but add the entry back.
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ return;
+ }
+ mIndexOfHeadersBackStack = -1;
+ if (!mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ }
+ mLastEntryCount = count;
+ }
+ }
+
+ /**
+ * Listener for transitions between browse headers and rows.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public static class BrowseTransitionListener {
+ /**
+ * Callback when headers transition starts.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStart(boolean withHeaders) {
+ }
+ /**
+ * Callback when headers transition stops.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStop(boolean withHeaders) {
+ }
+ }
+
+ private class SetSelectionRunnable implements Runnable {
+ static final int TYPE_INVALID = -1;
+ static final int TYPE_INTERNAL_SYNC = 0;
+ static final int TYPE_USER_REQUEST = 1;
+
+ private int mPosition;
+ private int mType;
+ private boolean mSmooth;
+
+ SetSelectionRunnable() {
+ reset();
+ }
+
+ void post(int position, int type, boolean smooth) {
+ // Posting the set selection, rather than calling it immediately, prevents an issue
+ // with adapter changes. Example: a row is added before the current selected row;
+ // first the fast lane view updates its selection, then the rows fragment has that
+ // new selection propagated immediately; THEN the rows view processes the same adapter
+ // change and moves the selection again.
+ if (type >= mType) {
+ mPosition = position;
+ mType = type;
+ mSmooth = smooth;
+ mBrowseFrame.removeCallbacks(this);
+ mBrowseFrame.post(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ setSelection(mPosition, mSmooth);
+ reset();
+ }
+
+ private void reset() {
+ mPosition = -1;
+ mType = TYPE_INVALID;
+ mSmooth = false;
+ }
+ }
+
+ /**
+ * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom
+ * fragments can interact with {@link BrowseFragment} using this interface.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public interface FragmentHost {
+ /**
+ * Fragments are required to invoke this callback once their view is created
+ * inside {@link Fragment#onViewCreated} method. {@link BrowseFragment} starts the entrance
+ * animation only after receiving this callback. Failure to invoke this method
+ * will lead to fragment not showing up.
+ *
+ * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
+ */
+ void notifyViewCreated(MainFragmentAdapter fragmentAdapter);
+
+ /**
+ * Fragments mapped to {@link PageRow} are required to invoke this callback once their data
+ * is created for transition, the entrance animation only after receiving this callback.
+ * Failure to invoke this method will lead to fragment not showing up.
+ *
+ * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
+ */
+ void notifyDataReady(MainFragmentAdapter fragmentAdapter);
+
+ /**
+ * Show or hide title view in {@link BrowseFragment} for fragments mapped to
+ * {@link PageRow}. Otherwise the request is ignored, in that case BrowseFragment is fully
+ * in control of showing/hiding title view.
+ * <p>
+ * When HeadersFragment is visible, BrowseFragment will hide search affordance view if
+ * there are other focusable rows above currently focused row.
+ *
+ * @param show Boolean indicating whether or not to show the title view.
+ */
+ void showTitleView(boolean show);
+ }
+
+ /**
+ * Default implementation of {@link FragmentHost} that is used only by
+ * {@link BrowseFragment}.
+ */
+ private final class FragmentHostImpl implements FragmentHost {
+ boolean mShowTitleView = true;
+
+ FragmentHostImpl() {
+ }
+
+ @Override
+ public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
+ mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ if (!mIsPageRow) {
+ // If it's not a PageRow: it's a ListRow, so we already have data ready.
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
+ }
+
+ @Override
+ public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
+ // If fragment host is not the currently active fragment (in BrowseFragment), then
+ // ignore the request.
+ if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
+ return;
+ }
+
+ // We only honor showTitle request for PageRows.
+ if (!mIsPageRow) {
+ return;
+ }
+
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
+
+ @Override
+ public void showTitleView(boolean show) {
+ mShowTitleView = show;
+
+ // If fragment host is not the currently active fragment (in BrowseFragment), then
+ // ignore the request.
+ if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
+ return;
+ }
+
+ // We only honor showTitle request for PageRows.
+ if (!mIsPageRow) {
+ return;
+ }
+
+ updateTitleViewVisibility();
+ }
+ }
+
+ /**
+ * Interface that defines the interaction between {@link BrowseFragment} and its main
+ * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
+ * it will be used to get the fragment to be shown in the content section. Clients can
+ * provide any implementation of fragment and customize its interaction with
+ * {@link BrowseFragment} by overriding the necessary methods.
+ *
+ * <p>
+ * Clients are expected to provide
+ * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
+ * implementations of {@link MainFragmentAdapter} for given content types. Currently
+ * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
+ * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
+ * {@link PageRow} - {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}.
+ *
+ * <p>
+ * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
+ * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
+ * and provide that through {@link MainFragmentAdapterRegistry}.
+ * {@link MainFragmentAdapter} implementation can supply any fragment and override
+ * just those interactions that makes sense.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public static class MainFragmentAdapter<T extends Fragment> {
+ private boolean mScalingEnabled;
+ private final T mFragment;
+ FragmentHostImpl mFragmentHost;
+
+ public MainFragmentAdapter(T fragment) {
+ this.mFragment = fragment;
+ }
+
+ public final T getFragment() {
+ return mFragment;
+ }
+
+ /**
+ * Returns whether its scrolling.
+ */
+ public boolean isScrolling() {
+ return false;
+ }
+
+ /**
+ * Set the visibility of titles/hover card of browse rows.
+ */
+ public void setExpand(boolean expand) {
+ }
+
+ /**
+ * For rows that willing to participate entrance transition, this function
+ * hide views if afterTransition is true, show views if afterTransition is false.
+ */
+ public void setEntranceTransitionState(boolean state) {
+ }
+
+ /**
+ * Sets the window alignment and also the pivots for scale operation.
+ */
+ public void setAlignment(int windowAlignOffsetFromTop) {
+ }
+
+ /**
+ * Callback indicating transition prepare start.
+ */
+ public boolean onTransitionPrepare() {
+ return false;
+ }
+
+ /**
+ * Callback indicating transition start.
+ */
+ public void onTransitionStart() {
+ }
+
+ /**
+ * Callback indicating transition end.
+ */
+ public void onTransitionEnd() {
+ }
+
+ /**
+ * Returns whether row scaling is enabled.
+ */
+ public boolean isScalingEnabled() {
+ return mScalingEnabled;
+ }
+
+ /**
+ * Sets the row scaling property.
+ */
+ public void setScalingEnabled(boolean scalingEnabled) {
+ this.mScalingEnabled = scalingEnabled;
+ }
+
+ /**
+ * Returns the current host interface so that main fragment can interact with
+ * {@link BrowseFragment}.
+ */
+ public final FragmentHost getFragmentHost() {
+ return mFragmentHost;
+ }
+
+ void setFragmentHost(FragmentHostImpl fragmentHost) {
+ this.mFragmentHost = fragmentHost;
+ }
+ }
+
+ /**
+ * Interface to be implemented by all fragments for providing an instance of
+ * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided
+ * against {@link PageRow} will need to implement this interface.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public interface MainFragmentAdapterProvider {
+ /**
+ * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment}
+ * would use to communicate with the target fragment.
+ */
+ MainFragmentAdapter getMainFragmentAdapter();
+ }
+
+ /**
+ * Interface to be implemented by {@link RowsFragment} and its subclasses for providing
+ * an instance of {@link MainFragmentRowsAdapter}.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public interface MainFragmentRowsAdapterProvider {
+ /**
+ * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment}
+ * would use to communicate with the target fragment.
+ */
+ MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ }
+
+ /**
+ * This is used to pass information to {@link RowsFragment} or its subclasses.
+ * {@link BrowseFragment} uses this interface to pass row based interaction events to
+ * the target fragment.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public static class MainFragmentRowsAdapter<T extends Fragment> {
+ private final T mFragment;
+
+ public MainFragmentRowsAdapter(T fragment) {
+ if (fragment == null) {
+ throw new IllegalArgumentException("Fragment can't be null");
+ }
+ this.mFragment = fragment;
+ }
+
+ public final T getFragment() {
+ return mFragment;
+ }
+ /**
+ * Set the visibility titles/hover of browse rows.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ }
+
+ /**
+ * Selects a Row and perform an optional task on the Row.
+ */
+ public void setSelectedPosition(int rowPosition,
+ boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ }
+
+ /**
+ * Selects a Row.
+ */
+ public void setSelectedPosition(int rowPosition, boolean smooth) {
+ }
+
+ /**
+ * @return The position of selected row.
+ */
+ public int getSelectedPosition() {
+ return 0;
+ }
+
+ /**
+ * @param position Position of Row.
+ * @return Row ViewHolder.
+ */
+ public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
+ return null;
+ }
+ }
+
+ private boolean createMainFragment(ObjectAdapter adapter, int position) {
+ Object item = null;
+ if (!mCanShowHeaders) {
+ // when header is disabled, we can decide to use RowsFragment even no data.
+ } else if (adapter == null || adapter.size() == 0) {
+ return false;
+ } else {
+ if (position < 0) {
+ position = 0;
+ } else if (position >= adapter.size()) {
+ throw new IllegalArgumentException(
+ String.format("Invalid position %d requested", position));
+ }
+ item = adapter.get(position);
+ }
+
+ boolean oldIsPageRow = mIsPageRow;
+ Object oldPageRow = mPageRow;
+ mIsPageRow = mCanShowHeaders && item instanceof PageRow;
+ mPageRow = mIsPageRow ? item : null;
+ boolean swap;
+
+ if (mMainFragment == null) {
+ swap = true;
+ } else {
+ if (oldIsPageRow) {
+ if (mIsPageRow) {
+ if (oldPageRow == null) {
+ // fragment is restored, page row object not yet set, so just set the
+ // mPageRow object and there is no need to replace the fragment
+ swap = false;
+ } else {
+ // swap if page row object changes
+ swap = oldPageRow != mPageRow;
+ }
+ } else {
+ swap = true;
+ }
+ } else {
+ swap = mIsPageRow;
+ }
+ }
+
+ if (swap) {
+ mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
+ if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {
+ throw new IllegalArgumentException(
+ "Fragment must implement MainFragmentAdapterProvider");
+ }
+
+ setMainFragmentAdapter();
+ }
+
+ return swap;
+ }
+
+ void setMainFragmentAdapter() {
+ mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)
+ .getMainFragmentAdapter();
+ mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+ if (!mIsPageRow) {
+ if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
+ setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)
+ .getMainFragmentRowsAdapter());
+ } else {
+ setMainFragmentRowsAdapter(null);
+ }
+ mIsPageRow = mMainFragmentRowsAdapter == null;
+ } else {
+ setMainFragmentRowsAdapter(null);
+ }
+ }
+
+ /**
+ * Factory class responsible for creating fragment given the current item. {@link ListRow}
+ * should return {@link RowsFragment} or its subclass whereas {@link PageRow}
+ * can return any fragment class.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public abstract static class FragmentFactory<T extends Fragment> {
+ public abstract T createFragment(Object row);
+ }
+
+ /**
+ * FragmentFactory implementation for {@link ListRow}.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {
+ @Override
+ public RowsFragment createFragment(Object row) {
+ return new RowsFragment();
+ }
+ }
+
+ /**
+ * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
+ * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
+ * handling {@link ListRow}. Developers can override that and also if they want to
+ * use custom fragment, they can register a custom {@link FragmentFactory}
+ * against {@link PageRow}.
+ * @deprecated use {@link BrowseSupportFragment}
+ */
+ @Deprecated
+ public final static class MainFragmentAdapterRegistry {
+ private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();
+ private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
+
+ public MainFragmentAdapterRegistry() {
+ registerFragment(ListRow.class, sDefaultFragmentFactory);
+ }
+
+ public void registerFragment(Class rowClass, FragmentFactory factory) {
+ mItemToFragmentFactoryMapping.put(rowClass, factory);
+ }
+
+ public Fragment createFragment(Object item) {
+ FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :
+ mItemToFragmentFactoryMapping.get(item.getClass());
+ if (fragmentFactory == null && !(item instanceof PageRow)) {
+ fragmentFactory = sDefaultFragmentFactory;
+ }
+
+ return fragmentFactory.createFragment(item);
+ }
+ }
+
+ static final String TAG = "BrowseFragment";
+
+ private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
+
+ static boolean DEBUG = false;
+
+ /** The headers fragment is enabled and shown by default. */
+ public static final int HEADERS_ENABLED = 1;
+
+ /** The headers fragment is enabled and hidden by default. */
+ public static final int HEADERS_HIDDEN = 2;
+
+ /** The headers fragment is disabled and will never be shown. */
+ public static final int HEADERS_DISABLED = 3;
+
+ private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =
+ new MainFragmentAdapterRegistry();
+ MainFragmentAdapter mMainFragmentAdapter;
+ Fragment mMainFragment;
+ HeadersFragment mHeadersFragment;
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ ListRowDataAdapter mMainFragmentListRowDataAdapter;
+
+ private ObjectAdapter mAdapter;
+ private PresenterSelector mAdapterPresenter;
+
+ private int mHeadersState = HEADERS_ENABLED;
+ private int mBrandColor = Color.TRANSPARENT;
+ private boolean mBrandColorSet;
+
+ BrowseFrameLayout mBrowseFrame;
+ private ScaleFrameLayout mScaleFrameLayout;
+ boolean mHeadersBackStackEnabled = true;
+ String mWithHeadersBackStackName;
+ boolean mShowingHeaders = true;
+ boolean mCanShowHeaders = true;
+ private int mContainerListMarginStart;
+ private int mContainerListAlignTop;
+ private boolean mMainFragmentScaleEnabled = true;
+ OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
+ private int mSelectedPosition = -1;
+ private float mScaleFactor;
+ boolean mIsPageRow;
+ Object mPageRow;
+
+ private PresenterSelector mHeaderPresenterSelector;
+ private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
+
+ // transition related:
+ Object mSceneWithHeaders;
+ Object mSceneWithoutHeaders;
+ private Object mSceneAfterEntranceTransition;
+ Object mHeadersTransition;
+ BackStackListener mBackStackChangedListener;
+ BrowseTransitionListener mBrowseTransitionListener;
+
+ private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
+ private static final String ARG_HEADERS_STATE =
+ BrowseFragment.class.getCanonicalName() + ".headersState";
+
+ /**
+ * Creates arguments for a browse fragment.
+ *
+ * @param args The Bundle to place arguments into, or null if the method
+ * should return a new Bundle.
+ * @param title The title of the BrowseFragment.
+ * @param headersState The initial state of the headers of the
+ * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
+ * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
+ * @return A Bundle with the given arguments for creating a BrowseFragment.
+ */
+ public static Bundle createArgs(Bundle args, String title, int headersState) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_TITLE, title);
+ args.putInt(ARG_HEADERS_STATE, headersState);
+ return args;
+ }
+
+ /**
+ * Sets the brand color for the browse fragment. The brand color is used as
+ * the primary color for UI elements in the browse fragment. For example,
+ * the background color of the headers fragment uses the brand color.
+ *
+ * @param color The color to use as the brand color of the fragment.
+ */
+ public void setBrandColor(@ColorInt int color) {
+ mBrandColor = color;
+ mBrandColorSet = true;
+
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setBackgroundColor(mBrandColor);
+ }
+ }
+
+ /**
+ * Returns the brand color for the browse fragment.
+ * The default is transparent.
+ */
+ @ColorInt
+ public int getBrandColor() {
+ return mBrandColor;
+ }
+
+ /**
+ * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
+ * DividerRow and PageRow.
+ */
+ private void updateWrapperPresenter() {
+ if (mAdapter == null) {
+ mAdapterPresenter = null;
+ return;
+ }
+ final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
+ if (adapterPresenter == null) {
+ throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
+ }
+ if (adapterPresenter == mAdapterPresenter) {
+ return;
+ }
+ mAdapterPresenter = adapterPresenter;
+
+ Presenter[] presenters = adapterPresenter.getPresenters();
+ final Presenter invisibleRowPresenter = new InvisibleRowPresenter();
+ final Presenter[] allPresenters = new Presenter[presenters.length + 1];
+ System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);
+ allPresenters[allPresenters.length - 1] = invisibleRowPresenter;
+ mAdapter.setPresenterSelector(new PresenterSelector() {
+ @Override
+ public Presenter getPresenter(Object item) {
+ Row row = (Row) item;
+ if (row.isRenderedAsRowView()) {
+ return adapterPresenter.getPresenter(item);
+ } else {
+ return invisibleRowPresenter;
+ }
+ }
+
+ @Override
+ public Presenter[] getPresenters() {
+ return allPresenters;
+ }
+ });
+ }
+
+ /**
+ * Sets the adapter containing the rows for the fragment.
+ *
+ * <p>The items referenced by the adapter must be be derived from
+ * {@link Row}. These rows will be used by the rows fragment and the headers
+ * fragment (if not disabled) to render the browse rows.
+ *
+ * @param adapter An ObjectAdapter for the browse rows. All items must
+ * derive from {@link Row}.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ updateWrapperPresenter();
+ if (getView() == null) {
+ return;
+ }
+
+ updateMainFragmentRowsAdapter();
+ mHeadersFragment.setAdapter(mAdapter);
+ }
+
+ void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+ if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+ return;
+ }
+ // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter
+ if (mMainFragmentRowsAdapter != null) {
+ // RowsFragment cannot change click/select listeners after view created.
+ // The main fragment and adapter should be GCed as long as there is no reference from
+ // BrowseFragment to it.
+ mMainFragmentRowsAdapter.setAdapter(null);
+ }
+ mMainFragmentRowsAdapter = mainFragmentRowsAdapter;
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
+ new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
+ mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter
+ updateMainFragmentRowsAdapter();
+ }
+
+ /**
+ * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.
+ * It also clears old mMainFragmentListRowDataAdapter.
+ */
+ void updateMainFragmentRowsAdapter() {
+ if (mMainFragmentListRowDataAdapter != null) {
+ mMainFragmentListRowDataAdapter.detach();
+ mMainFragmentListRowDataAdapter = null;
+ }
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentListRowDataAdapter = mAdapter == null
+ ? null : new ListRowDataAdapter(mAdapter);
+ mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);
+ }
+ }
+
+ public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
+ return mMainFragmentAdapterRegistry;
+ }
+
+ /**
+ * Returns the adapter containing the rows for the fragment.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mExternalOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Returns an item selection listener.
+ */
+ public OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mExternalOnItemViewSelectedListener;
+ }
+
+ /**
+ * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
+ * not been created yet or a different fragment is bound to it.
+ *
+ * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
+ */
+ public RowsFragment getRowsFragment() {
+ if (mMainFragment instanceof RowsFragment) {
+ return (RowsFragment) mMainFragment;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Current main fragment or null if not created.
+ */
+ public Fragment getMainFragment() {
+ return mMainFragment;
+ }
+
+ /**
+ * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
+ * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
+ */
+ public HeadersFragment getHeadersFragment() {
+ return mHeadersFragment;
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
+ }
+ }
+
+ /**
+ * Returns the item Clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
+ * Starts a headers transition.
+ *
+ * <p>This method will begin a transition to either show or hide the
+ * headers, depending on the value of withHeaders. If headers are disabled
+ * for this browse fragment, this method will throw an exception.
+ *
+ * @param withHeaders True if the headers should transition to being shown,
+ * false if the transition should result in headers being hidden.
+ */
+ public void startHeadersTransition(boolean withHeaders) {
+ if (!mCanShowHeaders) {
+ throw new IllegalStateException("Cannot start headers transition");
+ }
+ if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
+ return;
+ }
+ startHeadersTransitionInternal(withHeaders);
+ }
+
+ /**
+ * Returns true if the headers transition is currently running.
+ */
+ public boolean isInHeadersTransition() {
+ return mHeadersTransition != null;
+ }
+
+ /**
+ * Returns true if headers are shown.
+ */
+ public boolean isShowingHeaders() {
+ return mShowingHeaders;
+ }
+
+ /**
+ * Sets a listener for browse fragment transitions.
+ *
+ * @param listener The listener to call when a browse headers transition
+ * begins or ends.
+ */
+ public void setBrowseTransitionListener(BrowseTransitionListener listener) {
+ mBrowseTransitionListener = listener;
+ }
+
+ /**
+ * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
+ *
+ * @param enable true to enable row scaling
+ */
+ @Deprecated
+ public void enableRowScaling(boolean enable) {
+ enableMainFragmentScaling(enable);
+ }
+
+ /**
+ * Enables scaling of main fragment when headers are present. For the page/row fragment,
+ * scaling is enabled only when both this method and
+ * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
+ *
+ * @param enable true to enable row scaling
+ */
+ public void enableMainFragmentScaling(boolean enable) {
+ mMainFragmentScaleEnabled = enable;
+ }
+
+ void startHeadersTransitionInternal(final boolean withHeaders) {
+ if (getFragmentManager().isDestroyed()) {
+ return;
+ }
+ if (!isHeadersDataReady()) {
+ return;
+ }
+ mShowingHeaders = withHeaders;
+ mMainFragmentAdapter.onTransitionPrepare();
+ mMainFragmentAdapter.onTransitionStart();
+ onExpandTransitionStart(!withHeaders, new Runnable() {
+ @Override
+ public void run() {
+ mHeadersFragment.onTransitionPrepare();
+ mHeadersFragment.onTransitionStart();
+ createHeadersTransition();
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
+ }
+ TransitionHelper.runTransition(
+ withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
+ if (mHeadersBackStackEnabled) {
+ if (!withHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ } else {
+ int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
+ if (index >= 0) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
+ getFragmentManager().popBackStackImmediate(entry.getId(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ boolean isVerticalScrolling() {
+ // don't run transition
+ return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
+ }
+
+
+ private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
+ new BrowseFrameLayout.OnFocusSearchListener() {
+ @Override
+ public View onFocusSearch(View focused, int direction) {
+ // if headers is running transition, focus stays
+ if (mCanShowHeaders && isInHeadersTransition()) {
+ return focused;
+ }
+ if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
+
+ if (getTitleView() != null && focused != getTitleView()
+ && direction == View.FOCUS_UP) {
+ return getTitleView();
+ }
+ if (getTitleView() != null && getTitleView().hasFocus()
+ && direction == View.FOCUS_DOWN) {
+ return mCanShowHeaders && mShowingHeaders
+ ? mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
+ }
+
+ boolean isRtl = ViewCompat.getLayoutDirection(focused)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
+ if (mCanShowHeaders && direction == towardStart) {
+ if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {
+ return focused;
+ }
+ return mHeadersFragment.getVerticalGridView();
+ } else if (direction == towardEnd) {
+ if (isVerticalScrolling()) {
+ return focused;
+ } else if (mMainFragment != null && mMainFragment.getView() != null) {
+ return mMainFragment.getView();
+ }
+ return focused;
+ } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {
+ // disable focus_down moving into PageFragment.
+ return focused;
+ } else {
+ return null;
+ }
+ }
+ };
+
+ final boolean isHeadersDataReady() {
+ return mAdapter != null && mAdapter.size() != 0;
+ }
+
+ private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
+ new BrowseFrameLayout.OnChildFocusListener() {
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (getChildFragmentManager().isDestroyed()) {
+ return true;
+ }
+ // Make sure not changing focus when requestFocus() is called.
+ if (mCanShowHeaders && mShowingHeaders) {
+ if (mHeadersFragment != null && mHeadersFragment.getView() != null
+ && mHeadersFragment.getView().requestFocus(
+ direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ if (mMainFragment != null && mMainFragment.getView() != null
+ && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ return getTitleView() != null
+ && getTitleView().requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public void onRequestChildFocus(View child, View focused) {
+ if (getChildFragmentManager().isDestroyed()) {
+ return;
+ }
+ if (!mCanShowHeaders || isInHeadersTransition()) return;
+ int childId = child.getId();
+ if (childId == R.id.browse_container_dock && mShowingHeaders) {
+ startHeadersTransitionInternal(false);
+ } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ };
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
+ outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
+
+ if (mBackStackChangedListener != null) {
+ mBackStackChangedListener.save(outState);
+ } else {
+ outState.putBoolean(HEADER_SHOW, mShowingHeaders);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final Context context = FragmentUtil.getContext(BrowseFragment.this);
+ TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
+ mContainerListMarginStart = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
+ .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
+ mContainerListAlignTop = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
+ .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
+ ta.recycle();
+
+ readArguments(getArguments());
+
+ if (mCanShowHeaders) {
+ if (mHeadersBackStackEnabled) {
+ mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
+ mBackStackChangedListener = new BackStackListener();
+ getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
+ mBackStackChangedListener.load(savedInstanceState);
+ } else {
+ if (savedInstanceState != null) {
+ mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
+ }
+ }
+ }
+
+ mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
+ }
+
+ @Override
+ public void onDestroyView() {
+ setMainFragmentRowsAdapter(null);
+ mPageRow = null;
+ mMainFragmentAdapter = null;
+ mMainFragment = null;
+ mHeadersFragment = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mBackStackChangedListener != null) {
+ getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Creates a new {@link HeadersFragment} instance. Subclass of BrowseFragment may override and
+ * return an instance of subclass of HeadersFragment, e.g. when app wants to replace presenter
+ * to render HeaderItem.
+ *
+ * @return A new instance of {@link HeadersFragment} or its subclass.
+ */
+ public HeadersFragment onCreateHeadersFragment() {
+ return new HeadersFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
+ mHeadersFragment = onCreateHeadersFragment();
+
+ createMainFragment(mAdapter, mSelectedPosition);
+ FragmentTransaction ft = getChildFragmentManager().beginTransaction()
+ .replace(R.id.browse_headers_dock, mHeadersFragment);
+
+ if (mMainFragment != null) {
+ ft.replace(R.id.scale_frame, mMainFragment);
+ } else {
+ // Empty adapter used to guard against lazy adapter loading. When this
+ // fragment is instantiated, mAdapter might not have the data or might not
+ // have been set. In either of those cases mFragmentAdapter will be null.
+ // This way we can maintain the invariant that mMainFragmentAdapter is never
+ // null and it avoids doing null checks all over the code.
+ mMainFragmentAdapter = new MainFragmentAdapter(null);
+ mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+ }
+
+ ft.commit();
+ } else {
+ mHeadersFragment = (HeadersFragment) getChildFragmentManager()
+ .findFragmentById(R.id.browse_headers_dock);
+ mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
+
+ mIsPageRow = savedInstanceState != null
+ && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
+ // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is
+ // the case for restoring, later if setSelection() triggers a createMainFragment(),
+ // should not create fragment.
+
+ mSelectedPosition = savedInstanceState != null
+ ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
+
+ setMainFragmentAdapter();
+ }
+
+ mHeadersFragment.setHeadersGone(!mCanShowHeaders);
+ if (mHeaderPresenterSelector != null) {
+ mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
+ mHeadersFragment.setAdapter(mAdapter);
+ mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
+ mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
+
+ View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
+
+ getProgressBarManager().setRootView((ViewGroup)root);
+
+ mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
+ mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
+ mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
+
+ installTitleView(inflater, mBrowseFrame, savedInstanceState);
+
+ mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
+ mScaleFrameLayout.setPivotX(0);
+ mScaleFrameLayout.setPivotY(mContainerListAlignTop);
+
+ if (mBrandColorSet) {
+ mHeadersFragment.setBackgroundColor(mBrandColor);
+ }
+
+ mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showHeaders(true);
+ }
+ });
+ mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showHeaders(false);
+ }
+ });
+ mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ setEntranceTransitionEndState();
+ }
+ });
+
+ return root;
+ }
+
+ void createHeadersTransition() {
+ mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),
+ mShowingHeaders
+ ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
+
+ TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ }
+ @Override
+ public void onTransitionEnd(Object transition) {
+ mHeadersTransition = null;
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.onTransitionEnd();
+ if (!mShowingHeaders && mMainFragment != null) {
+ View mainFragmentView = mMainFragment.getView();
+ if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
+ mainFragmentView.requestFocus();
+ }
+ }
+ }
+ if (mHeadersFragment != null) {
+ mHeadersFragment.onTransitionEnd();
+ if (mShowingHeaders) {
+ VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
+ if (headerGridView != null && !headerGridView.hasFocus()) {
+ headerGridView.requestFocus();
+ }
+ }
+ }
+
+ // Animate TitleView once header animation is complete.
+ updateTitleViewVisibility();
+
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
+ }
+ }
+ });
+ }
+
+ void updateTitleViewVisibility() {
+ if (!mShowingHeaders) {
+ boolean showTitleView;
+ if (mIsPageRow && mMainFragmentAdapter != null) {
+ // page fragment case:
+ showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
+ } else {
+ // regular row view case:
+ showTitleView = isFirstRowWithContent(mSelectedPosition);
+ }
+ if (showTitleView) {
+ showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);
+ } else {
+ showTitle(false);
+ }
+ } else {
+ // when HeaderFragment is showing, showBranding and showSearch are slightly different
+ boolean showBranding;
+ boolean showSearch;
+ if (mIsPageRow && mMainFragmentAdapter != null) {
+ showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
+ } else {
+ showBranding = isFirstRowWithContent(mSelectedPosition);
+ }
+ showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);
+ int flags = 0;
+ if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;
+ if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;
+ if (flags != 0) {
+ showTitle(flags);
+ } else {
+ showTitle(false);
+ }
+ }
+ }
+
+ boolean isFirstRowWithContentOrPageRow(int rowPosition) {
+ if (mAdapter == null || mAdapter.size() == 0) {
+ return true;
+ }
+ for (int i = 0; i < mAdapter.size(); i++) {
+ final Row row = (Row) mAdapter.get(i);
+ if (row.isRenderedAsRowView() || row instanceof PageRow) {
+ return rowPosition == i;
+ }
+ }
+ return true;
+ }
+
+ boolean isFirstRowWithContent(int rowPosition) {
+ if (mAdapter == null || mAdapter.size() == 0) {
+ return true;
+ }
+ for (int i = 0; i < mAdapter.size(); i++) {
+ final Row row = (Row) mAdapter.get(i);
+ if (row.isRenderedAsRowView()) {
+ return rowPosition == i;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the {@link PresenterSelector} used to render the row headers.
+ *
+ * @param headerPresenterSelector The PresenterSelector that will determine
+ * the Presenter for each row header.
+ */
+ public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
+ mHeaderPresenterSelector = headerPresenterSelector;
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
+ }
+
+ private void setHeadersOnScreen(boolean onScreen) {
+ MarginLayoutParams lp;
+ View containerList;
+ containerList = mHeadersFragment.getView();
+ lp = (MarginLayoutParams) containerList.getLayoutParams();
+ lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
+ containerList.setLayoutParams(lp);
+ }
+
+ void showHeaders(boolean show) {
+ if (DEBUG) Log.v(TAG, "showHeaders " + show);
+ mHeadersFragment.setHeadersEnabled(show);
+ setHeadersOnScreen(show);
+ expandMainFragment(!show);
+ }
+
+ private void expandMainFragment(boolean expand) {
+ MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
+ params.setMarginStart(!expand ? mContainerListMarginStart : 0);
+ mScaleFrameLayout.setLayoutParams(params);
+ mMainFragmentAdapter.setExpand(expand);
+
+ setMainFragmentAlignment();
+ final float scaleFactor = !expand
+ && mMainFragmentScaleEnabled
+ && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
+ mScaleFrameLayout.setLayoutScaleY(scaleFactor);
+ mScaleFrameLayout.setChildScale(scaleFactor);
+ }
+
+ private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
+ new HeadersFragment.OnHeaderClickedListener() {
+ @Override
+ public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
+ if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
+ return;
+ }
+ startHeadersTransitionInternal(false);
+ mMainFragment.getView().requestFocus();
+ }
+ };
+
+ class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+
+ public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
+ mMainFragmentRowsAdapter = fragmentRowsAdapter;
+ }
+
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ int position = mMainFragmentRowsAdapter.getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "row selected position " + position);
+ onRowSelected(position);
+ if (mExternalOnItemViewSelectedListener != null) {
+ mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
+ private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
+ new HeadersFragment.OnHeaderViewSelectedListener() {
+ @Override
+ public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
+ int position = mHeadersFragment.getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "header selected position " + position);
+ onRowSelected(position);
+ }
+ };
+
+ void onRowSelected(int position) {
+ // even position is same, it could be data changed, always post selection runnable
+ // to possibly swap main fragment.
+ mSetSelectionRunnable.post(
+ position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
+ }
+
+ void setSelection(int position, boolean smooth) {
+ if (position == NO_POSITION) {
+ return;
+ }
+
+ mSelectedPosition = position;
+ if (mHeadersFragment == null || mMainFragmentAdapter == null) {
+ // onDestroyView() called
+ return;
+ }
+ mHeadersFragment.setSelectedPosition(position, smooth);
+ replaceMainFragment(position);
+
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
+ }
+
+ updateTitleViewVisibility();
+ }
+
+ private void replaceMainFragment(int position) {
+ if (createMainFragment(mAdapter, position)) {
+ swapToMainFragment();
+ expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
+ }
+ }
+
+ private void swapToMainFragment() {
+ final VerticalGridView gridView = mHeadersFragment.getVerticalGridView();
+ if (isShowingHeaders() && gridView != null
+ && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
+ // if user is scrolling HeadersFragment, swap to empty fragment and wait scrolling
+ // finishes.
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.scale_frame, new Fragment()).commit();
+ gridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ gridView.removeOnScrollListener(this);
+ FragmentManager fm = getChildFragmentManager();
+ Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);
+ if (currentFragment != mMainFragment) {
+ fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit();
+ }
+ }
+ }
+ });
+ } else {
+ // Otherwise swap immediately
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.scale_frame, mMainFragment).commit();
+ }
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Gets position of currently selected row.
+ * @return Position of currently selected row.
+ */
+ public int getSelectedPosition() {
+ return mSelectedPosition;
+ }
+
+ /**
+ * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.
+ */
+ public RowPresenter.ViewHolder getSelectedRowViewHolder() {
+ if (mMainFragmentRowsAdapter != null) {
+ int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();
+ return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ mSetSelectionRunnable.post(
+ position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
+ }
+
+ /**
+ * Selects a Row and perform an optional task on the Row. For example
+ * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
+ * scrolls to 11th row and selects 6th item on that row. The method will be ignored if
+ * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
+ * ViewGroup, Bundle)}).
+ *
+ * @param rowPosition Which row to select.
+ * @param smooth True to scroll to the row, false for no animation.
+ * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers
+ * fragment will be collapsed.
+ */
+ public void setSelectedPosition(int rowPosition, boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ if (mMainFragmentAdapterRegistry == null) {
+ return;
+ }
+ if (rowHolderTask != null) {
+ startHeadersTransition(false);
+ }
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHeadersFragment.setAlignment(mContainerListAlignTop);
+ setMainFragmentAlignment();
+
+ if (mCanShowHeaders && mShowingHeaders && mHeadersFragment != null
+ && mHeadersFragment.getView() != null) {
+ mHeadersFragment.getView().requestFocus();
+ } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null
+ && mMainFragment.getView() != null) {
+ mMainFragment.getView().requestFocus();
+ }
+
+ if (mCanShowHeaders) {
+ showHeaders(mShowingHeaders);
+ }
+
+ mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
+ }
+
+ private void onExpandTransitionStart(boolean expand, final Runnable callback) {
+ if (expand) {
+ callback.run();
+ return;
+ }
+ // Run a "pre" layout when we go non-expand, in order to get the initial
+ // positions of added rows.
+ new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
+ }
+
+ private void setMainFragmentAlignment() {
+ int alignOffset = mContainerListAlignTop;
+ if (mMainFragmentScaleEnabled
+ && mMainFragmentAdapter.isScalingEnabled()
+ && mShowingHeaders) {
+ alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
+ }
+ mMainFragmentAdapter.setAlignment(alignOffset);
+ }
+
+ /**
+ * Enables/disables headers transition on back key support. This is enabled by
+ * default. The BrowseFragment will add a back stack entry when headers are
+ * showing. Running a headers transition when the back key is pressed only
+ * works when the headers state is {@link #HEADERS_ENABLED} or
+ * {@link #HEADERS_HIDDEN}.
+ * <p>
+ * NOTE: If an Activity has its own onBackPressed() handling, you must
+ * disable this feature. You may use {@link #startHeadersTransition(boolean)}
+ * and {@link BrowseTransitionListener} in your own back stack handling.
+ */
+ public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
+ mHeadersBackStackEnabled = headersBackStackEnabled;
+ }
+
+ /**
+ * Returns true if headers transition on back key support is enabled.
+ */
+ public final boolean isHeadersTransitionOnBackEnabled() {
+ return mHeadersBackStackEnabled;
+ }
+
+ private void readArguments(Bundle args) {
+ if (args == null) {
+ return;
+ }
+ if (args.containsKey(ARG_TITLE)) {
+ setTitle(args.getString(ARG_TITLE));
+ }
+ if (args.containsKey(ARG_HEADERS_STATE)) {
+ setHeadersState(args.getInt(ARG_HEADERS_STATE));
+ }
+ }
+
+ /**
+ * Sets the state for the headers column in the browse fragment. Must be one
+ * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
+ * {@link #HEADERS_DISABLED}.
+ *
+ * @param headersState The state of the headers for the browse fragment.
+ */
+ public void setHeadersState(int headersState) {
+ if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
+ throw new IllegalArgumentException("Invalid headers state: " + headersState);
+ }
+ if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
+
+ if (headersState != mHeadersState) {
+ mHeadersState = headersState;
+ switch (headersState) {
+ case HEADERS_ENABLED:
+ mCanShowHeaders = true;
+ mShowingHeaders = true;
+ break;
+ case HEADERS_HIDDEN:
+ mCanShowHeaders = true;
+ mShowingHeaders = false;
+ break;
+ case HEADERS_DISABLED:
+ mCanShowHeaders = false;
+ mShowingHeaders = false;
+ break;
+ default:
+ Log.w(TAG, "Unknown headers state: " + headersState);
+ break;
+ }
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setHeadersGone(!mCanShowHeaders);
+ }
+ }
+ }
+
+ /**
+ * Returns the state of the headers column in the browse fragment.
+ */
+ public int getHeadersState() {
+ return mHeadersState;
+ }
+
+ @Override
+ protected Object createEntranceTransition() {
+ return TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),
+ R.transition.lb_browse_entrance_transition);
+ }
+
+ @Override
+ protected void runEntranceTransition(Object entranceTransition) {
+ TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
+ }
+
+ @Override
+ protected void onEntranceTransitionPrepare() {
+ mHeadersFragment.onTransitionPrepare();
+ mMainFragmentAdapter.setEntranceTransitionState(false);
+ mMainFragmentAdapter.onTransitionPrepare();
+ }
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ mHeadersFragment.onTransitionStart();
+ mMainFragmentAdapter.onTransitionStart();
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.onTransitionEnd();
+ }
+
+ if (mHeadersFragment != null) {
+ mHeadersFragment.onTransitionEnd();
+ }
+ }
+
+ void setSearchOrbViewOnScreen(boolean onScreen) {
+ View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();
+ if (searchOrbView != null) {
+ MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
+ lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
+ searchOrbView.setLayoutParams(lp);
+ }
+ }
+
+ void setEntranceTransitionStartState() {
+ setHeadersOnScreen(false);
+ setSearchOrbViewOnScreen(false);
+ // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+ // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+ // one when setEntranceTransitionStartState() is called.
+ }
+
+ void setEntranceTransitionEndState() {
+ setHeadersOnScreen(mShowingHeaders);
+ setSearchOrbViewOnScreen(true);
+ mMainFragmentAdapter.setEntranceTransitionState(true);
+ }
+
+ private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
+
+ private final View mView;
+ private final Runnable mCallback;
+ private int mState;
+ private MainFragmentAdapter mainFragmentAdapter;
+
+ final static int STATE_INIT = 0;
+ final static int STATE_FIRST_DRAW = 1;
+ final static int STATE_SECOND_DRAW = 2;
+
+ ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
+ mView = view;
+ mCallback = callback;
+ mainFragmentAdapter = adapter;
+ }
+
+ void execute() {
+ mView.getViewTreeObserver().addOnPreDrawListener(this);
+ mainFragmentAdapter.setExpand(false);
+ // always trigger onPreDraw even adapter setExpand() does nothing.
+ mView.invalidate();
+ mState = STATE_INIT;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (getView() == null || FragmentUtil.getContext(BrowseFragment.this) == null) {
+ mView.getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ if (mState == STATE_INIT) {
+ mainFragmentAdapter.setExpand(true);
+ // always trigger onPreDraw even adapter setExpand() does nothing.
+ mView.invalidate();
+ mState = STATE_FIRST_DRAW;
+ } else if (mState == STATE_FIRST_DRAW) {
+ mCallback.run();
+ mView.getViewTreeObserver().removeOnPreDrawListener(this);
+ mState = STATE_SECOND_DRAW;
+ }
+ return false;
+ }
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
new file mode 100644
index 0000000..c28064c
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -0,0 +1,1845 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
+import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.InvisibleRowPresenter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowHeaderPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.ScaleFrameLayout;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.BackStackEntry;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewTreeObserver;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A fragment for creating Leanback browse screens. It is composed of a
+ * RowsSupportFragment and a HeadersSupportFragment.
+ * <p>
+ * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set
+ * of rows in a vertical list. The elements in this adapter must be subclasses
+ * of {@link Row}.
+ * <p>
+ * The HeadersSupportFragment can be set to be either shown or hidden by default, or
+ * may be disabled entirely. See {@link #setHeadersState} for details.
+ * <p>
+ * By default the BrowseSupportFragment includes support for returning to the headers
+ * when the user presses Back. For Activities that customize {@link
+ * android.support.v4.app.FragmentActivity#onBackPressed()}, you must disable this default Back key support by
+ * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
+ * use {@link BrowseSupportFragment.BrowseTransitionListener} and
+ * {@link #startHeadersTransition(boolean)}.
+ * <p>
+ * The recommended theme to use with a BrowseSupportFragment is
+ * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
+ * </p>
+ */
+public class BrowseSupportFragment extends BaseSupportFragment {
+
+ // BUNDLE attribute for saving header show/hide status when backstack is used:
+ static final String HEADER_STACK_INDEX = "headerStackIndex";
+ // BUNDLE attribute for saving header show/hide status when backstack is not used:
+ static final String HEADER_SHOW = "headerShow";
+ private static final String IS_PAGE_ROW = "isPageRow";
+ private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+
+ /**
+ * State to hide headers fragment.
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionStartState();
+ }
+ };
+
+ /**
+ * Event for Header fragment view is created, we could perform
+ * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+ */
+ final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+ /**
+ * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+ * {@link #onEntranceTransitionPrepare()}.
+ */
+ final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+ /**
+ * Event that data for the screen is ready, this is additional requirement to launch entrance
+ * transition.
+ */
+ final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ // when headers fragment view is created we could setEntranceTransitionStartState()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+ EVT_HEADER_VIEW_CREATED);
+
+ // add additional requirement for onEntranceTransitionPrepare()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ // add additional requirement to launch entrance transition.
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
+ EVT_SCREEN_DATA_READY);
+ }
+
+ final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
+ int mLastEntryCount;
+ int mIndexOfHeadersBackStack;
+
+ BackStackListener() {
+ mLastEntryCount = getFragmentManager().getBackStackEntryCount();
+ mIndexOfHeadersBackStack = -1;
+ }
+
+ void load(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
+ mShowingHeaders = mIndexOfHeadersBackStack == -1;
+ } else {
+ if (!mShowingHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ }
+ }
+ }
+
+ void save(Bundle outState) {
+ outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
+ }
+
+
+ @Override
+ public void onBackStackChanged() {
+ if (getFragmentManager() == null) {
+ Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
+ return;
+ }
+ int count = getFragmentManager().getBackStackEntryCount();
+ // if backstack is growing and last pushed entry is "headers" backstack,
+ // remember the index of the entry.
+ if (count > mLastEntryCount) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
+ if (mWithHeadersBackStackName.equals(entry.getName())) {
+ mIndexOfHeadersBackStack = count - 1;
+ }
+ } else if (count < mLastEntryCount) {
+ // if popped "headers" backstack, initiate the show header transition if needed
+ if (mIndexOfHeadersBackStack >= count) {
+ if (!isHeadersDataReady()) {
+ // if main fragment was restored first before BrowseSupportFragment's adapter gets
+ // restored: don't start header transition, but add the entry back.
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ return;
+ }
+ mIndexOfHeadersBackStack = -1;
+ if (!mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ }
+ mLastEntryCount = count;
+ }
+ }
+
+ /**
+ * Listener for transitions between browse headers and rows.
+ */
+ public static class BrowseTransitionListener {
+ /**
+ * Callback when headers transition starts.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStart(boolean withHeaders) {
+ }
+ /**
+ * Callback when headers transition stops.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStop(boolean withHeaders) {
+ }
+ }
+
+ private class SetSelectionRunnable implements Runnable {
+ static final int TYPE_INVALID = -1;
+ static final int TYPE_INTERNAL_SYNC = 0;
+ static final int TYPE_USER_REQUEST = 1;
+
+ private int mPosition;
+ private int mType;
+ private boolean mSmooth;
+
+ SetSelectionRunnable() {
+ reset();
+ }
+
+ void post(int position, int type, boolean smooth) {
+ // Posting the set selection, rather than calling it immediately, prevents an issue
+ // with adapter changes. Example: a row is added before the current selected row;
+ // first the fast lane view updates its selection, then the rows fragment has that
+ // new selection propagated immediately; THEN the rows view processes the same adapter
+ // change and moves the selection again.
+ if (type >= mType) {
+ mPosition = position;
+ mType = type;
+ mSmooth = smooth;
+ mBrowseFrame.removeCallbacks(this);
+ mBrowseFrame.post(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ setSelection(mPosition, mSmooth);
+ reset();
+ }
+
+ private void reset() {
+ mPosition = -1;
+ mType = TYPE_INVALID;
+ mSmooth = false;
+ }
+ }
+
+ /**
+ * Possible set of actions that {@link BrowseSupportFragment} exposes to clients. Custom
+ * fragments can interact with {@link BrowseSupportFragment} using this interface.
+ */
+ public interface FragmentHost {
+ /**
+ * Fragments are required to invoke this callback once their view is created
+ * inside {@link Fragment#onViewCreated} method. {@link BrowseSupportFragment} starts the entrance
+ * animation only after receiving this callback. Failure to invoke this method
+ * will lead to fragment not showing up.
+ *
+ * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
+ */
+ void notifyViewCreated(MainFragmentAdapter fragmentAdapter);
+
+ /**
+ * Fragments mapped to {@link PageRow} are required to invoke this callback once their data
+ * is created for transition, the entrance animation only after receiving this callback.
+ * Failure to invoke this method will lead to fragment not showing up.
+ *
+ * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
+ */
+ void notifyDataReady(MainFragmentAdapter fragmentAdapter);
+
+ /**
+ * Show or hide title view in {@link BrowseSupportFragment} for fragments mapped to
+ * {@link PageRow}. Otherwise the request is ignored, in that case BrowseSupportFragment is fully
+ * in control of showing/hiding title view.
+ * <p>
+ * When HeadersSupportFragment is visible, BrowseSupportFragment will hide search affordance view if
+ * there are other focusable rows above currently focused row.
+ *
+ * @param show Boolean indicating whether or not to show the title view.
+ */
+ void showTitleView(boolean show);
+ }
+
+ /**
+ * Default implementation of {@link FragmentHost} that is used only by
+ * {@link BrowseSupportFragment}.
+ */
+ private final class FragmentHostImpl implements FragmentHost {
+ boolean mShowTitleView = true;
+
+ FragmentHostImpl() {
+ }
+
+ @Override
+ public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
+ mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ if (!mIsPageRow) {
+ // If it's not a PageRow: it's a ListRow, so we already have data ready.
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
+ }
+
+ @Override
+ public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
+ // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
+ // ignore the request.
+ if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
+ return;
+ }
+
+ // We only honor showTitle request for PageRows.
+ if (!mIsPageRow) {
+ return;
+ }
+
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
+
+ @Override
+ public void showTitleView(boolean show) {
+ mShowTitleView = show;
+
+ // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
+ // ignore the request.
+ if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
+ return;
+ }
+
+ // We only honor showTitle request for PageRows.
+ if (!mIsPageRow) {
+ return;
+ }
+
+ updateTitleViewVisibility();
+ }
+ }
+
+ /**
+ * Interface that defines the interaction between {@link BrowseSupportFragment} and its main
+ * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
+ * it will be used to get the fragment to be shown in the content section. Clients can
+ * provide any implementation of fragment and customize its interaction with
+ * {@link BrowseSupportFragment} by overriding the necessary methods.
+ *
+ * <p>
+ * Clients are expected to provide
+ * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
+ * implementations of {@link MainFragmentAdapter} for given content types. Currently
+ * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
+ * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
+ * {@link PageRow} - {@link android.support.v17.leanback.app.RowsSupportFragment.MainFragmentAdapter}.
+ *
+ * <p>
+ * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
+ * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
+ * and provide that through {@link MainFragmentAdapterRegistry}.
+ * {@link MainFragmentAdapter} implementation can supply any fragment and override
+ * just those interactions that makes sense.
+ */
+ public static class MainFragmentAdapter<T extends Fragment> {
+ private boolean mScalingEnabled;
+ private final T mFragment;
+ FragmentHostImpl mFragmentHost;
+
+ public MainFragmentAdapter(T fragment) {
+ this.mFragment = fragment;
+ }
+
+ public final T getFragment() {
+ return mFragment;
+ }
+
+ /**
+ * Returns whether its scrolling.
+ */
+ public boolean isScrolling() {
+ return false;
+ }
+
+ /**
+ * Set the visibility of titles/hover card of browse rows.
+ */
+ public void setExpand(boolean expand) {
+ }
+
+ /**
+ * For rows that willing to participate entrance transition, this function
+ * hide views if afterTransition is true, show views if afterTransition is false.
+ */
+ public void setEntranceTransitionState(boolean state) {
+ }
+
+ /**
+ * Sets the window alignment and also the pivots for scale operation.
+ */
+ public void setAlignment(int windowAlignOffsetFromTop) {
+ }
+
+ /**
+ * Callback indicating transition prepare start.
+ */
+ public boolean onTransitionPrepare() {
+ return false;
+ }
+
+ /**
+ * Callback indicating transition start.
+ */
+ public void onTransitionStart() {
+ }
+
+ /**
+ * Callback indicating transition end.
+ */
+ public void onTransitionEnd() {
+ }
+
+ /**
+ * Returns whether row scaling is enabled.
+ */
+ public boolean isScalingEnabled() {
+ return mScalingEnabled;
+ }
+
+ /**
+ * Sets the row scaling property.
+ */
+ public void setScalingEnabled(boolean scalingEnabled) {
+ this.mScalingEnabled = scalingEnabled;
+ }
+
+ /**
+ * Returns the current host interface so that main fragment can interact with
+ * {@link BrowseSupportFragment}.
+ */
+ public final FragmentHost getFragmentHost() {
+ return mFragmentHost;
+ }
+
+ void setFragmentHost(FragmentHostImpl fragmentHost) {
+ this.mFragmentHost = fragmentHost;
+ }
+ }
+
+ /**
+ * Interface to be implemented by all fragments for providing an instance of
+ * {@link MainFragmentAdapter}. Both {@link RowsSupportFragment} and custom fragment provided
+ * against {@link PageRow} will need to implement this interface.
+ */
+ public interface MainFragmentAdapterProvider {
+ /**
+ * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseSupportFragment}
+ * would use to communicate with the target fragment.
+ */
+ MainFragmentAdapter getMainFragmentAdapter();
+ }
+
+ /**
+ * Interface to be implemented by {@link RowsSupportFragment} and its subclasses for providing
+ * an instance of {@link MainFragmentRowsAdapter}.
+ */
+ public interface MainFragmentRowsAdapterProvider {
+ /**
+ * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseSupportFragment}
+ * would use to communicate with the target fragment.
+ */
+ MainFragmentRowsAdapter getMainFragmentRowsAdapter();
+ }
+
+ /**
+ * This is used to pass information to {@link RowsSupportFragment} or its subclasses.
+ * {@link BrowseSupportFragment} uses this interface to pass row based interaction events to
+ * the target fragment.
+ */
+ public static class MainFragmentRowsAdapter<T extends Fragment> {
+ private final T mFragment;
+
+ public MainFragmentRowsAdapter(T fragment) {
+ if (fragment == null) {
+ throw new IllegalArgumentException("Fragment can't be null");
+ }
+ this.mFragment = fragment;
+ }
+
+ public final T getFragment() {
+ return mFragment;
+ }
+ /**
+ * Set the visibility titles/hover of browse rows.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ }
+
+ /**
+ * Selects a Row and perform an optional task on the Row.
+ */
+ public void setSelectedPosition(int rowPosition,
+ boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ }
+
+ /**
+ * Selects a Row.
+ */
+ public void setSelectedPosition(int rowPosition, boolean smooth) {
+ }
+
+ /**
+ * @return The position of selected row.
+ */
+ public int getSelectedPosition() {
+ return 0;
+ }
+
+ /**
+ * @param position Position of Row.
+ * @return Row ViewHolder.
+ */
+ public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
+ return null;
+ }
+ }
+
+ private boolean createMainFragment(ObjectAdapter adapter, int position) {
+ Object item = null;
+ if (!mCanShowHeaders) {
+ // when header is disabled, we can decide to use RowsSupportFragment even no data.
+ } else if (adapter == null || adapter.size() == 0) {
+ return false;
+ } else {
+ if (position < 0) {
+ position = 0;
+ } else if (position >= adapter.size()) {
+ throw new IllegalArgumentException(
+ String.format("Invalid position %d requested", position));
+ }
+ item = adapter.get(position);
+ }
+
+ boolean oldIsPageRow = mIsPageRow;
+ Object oldPageRow = mPageRow;
+ mIsPageRow = mCanShowHeaders && item instanceof PageRow;
+ mPageRow = mIsPageRow ? item : null;
+ boolean swap;
+
+ if (mMainFragment == null) {
+ swap = true;
+ } else {
+ if (oldIsPageRow) {
+ if (mIsPageRow) {
+ if (oldPageRow == null) {
+ // fragment is restored, page row object not yet set, so just set the
+ // mPageRow object and there is no need to replace the fragment
+ swap = false;
+ } else {
+ // swap if page row object changes
+ swap = oldPageRow != mPageRow;
+ }
+ } else {
+ swap = true;
+ }
+ } else {
+ swap = mIsPageRow;
+ }
+ }
+
+ if (swap) {
+ mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
+ if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {
+ throw new IllegalArgumentException(
+ "Fragment must implement MainFragmentAdapterProvider");
+ }
+
+ setMainFragmentAdapter();
+ }
+
+ return swap;
+ }
+
+ void setMainFragmentAdapter() {
+ mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)
+ .getMainFragmentAdapter();
+ mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+ if (!mIsPageRow) {
+ if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
+ setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)
+ .getMainFragmentRowsAdapter());
+ } else {
+ setMainFragmentRowsAdapter(null);
+ }
+ mIsPageRow = mMainFragmentRowsAdapter == null;
+ } else {
+ setMainFragmentRowsAdapter(null);
+ }
+ }
+
+ /**
+ * Factory class responsible for creating fragment given the current item. {@link ListRow}
+ * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow}
+ * can return any fragment class.
+ */
+ public abstract static class FragmentFactory<T extends Fragment> {
+ public abstract T createFragment(Object row);
+ }
+
+ /**
+ * FragmentFactory implementation for {@link ListRow}.
+ */
+ public static class ListRowFragmentFactory extends FragmentFactory<RowsSupportFragment> {
+ @Override
+ public RowsSupportFragment createFragment(Object row) {
+ return new RowsSupportFragment();
+ }
+ }
+
+ /**
+ * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
+ * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
+ * handling {@link ListRow}. Developers can override that and also if they want to
+ * use custom fragment, they can register a custom {@link FragmentFactory}
+ * against {@link PageRow}.
+ */
+ public final static class MainFragmentAdapterRegistry {
+ private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();
+ private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
+
+ public MainFragmentAdapterRegistry() {
+ registerFragment(ListRow.class, sDefaultFragmentFactory);
+ }
+
+ public void registerFragment(Class rowClass, FragmentFactory factory) {
+ mItemToFragmentFactoryMapping.put(rowClass, factory);
+ }
+
+ public Fragment createFragment(Object item) {
+ FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :
+ mItemToFragmentFactoryMapping.get(item.getClass());
+ if (fragmentFactory == null && !(item instanceof PageRow)) {
+ fragmentFactory = sDefaultFragmentFactory;
+ }
+
+ return fragmentFactory.createFragment(item);
+ }
+ }
+
+ static final String TAG = "BrowseSupportFragment";
+
+ private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
+
+ static boolean DEBUG = false;
+
+ /** The headers fragment is enabled and shown by default. */
+ public static final int HEADERS_ENABLED = 1;
+
+ /** The headers fragment is enabled and hidden by default. */
+ public static final int HEADERS_HIDDEN = 2;
+
+ /** The headers fragment is disabled and will never be shown. */
+ public static final int HEADERS_DISABLED = 3;
+
+ private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =
+ new MainFragmentAdapterRegistry();
+ MainFragmentAdapter mMainFragmentAdapter;
+ Fragment mMainFragment;
+ HeadersSupportFragment mHeadersSupportFragment;
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ ListRowDataAdapter mMainFragmentListRowDataAdapter;
+
+ private ObjectAdapter mAdapter;
+ private PresenterSelector mAdapterPresenter;
+
+ private int mHeadersState = HEADERS_ENABLED;
+ private int mBrandColor = Color.TRANSPARENT;
+ private boolean mBrandColorSet;
+
+ BrowseFrameLayout mBrowseFrame;
+ private ScaleFrameLayout mScaleFrameLayout;
+ boolean mHeadersBackStackEnabled = true;
+ String mWithHeadersBackStackName;
+ boolean mShowingHeaders = true;
+ boolean mCanShowHeaders = true;
+ private int mContainerListMarginStart;
+ private int mContainerListAlignTop;
+ private boolean mMainFragmentScaleEnabled = true;
+ OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
+ private int mSelectedPosition = -1;
+ private float mScaleFactor;
+ boolean mIsPageRow;
+ Object mPageRow;
+
+ private PresenterSelector mHeaderPresenterSelector;
+ private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
+
+ // transition related:
+ Object mSceneWithHeaders;
+ Object mSceneWithoutHeaders;
+ private Object mSceneAfterEntranceTransition;
+ Object mHeadersTransition;
+ BackStackListener mBackStackChangedListener;
+ BrowseTransitionListener mBrowseTransitionListener;
+
+ private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title";
+ private static final String ARG_HEADERS_STATE =
+ BrowseSupportFragment.class.getCanonicalName() + ".headersState";
+
+ /**
+ * Creates arguments for a browse fragment.
+ *
+ * @param args The Bundle to place arguments into, or null if the method
+ * should return a new Bundle.
+ * @param title The title of the BrowseSupportFragment.
+ * @param headersState The initial state of the headers of the
+ * BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
+ * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
+ * @return A Bundle with the given arguments for creating a BrowseSupportFragment.
+ */
+ public static Bundle createArgs(Bundle args, String title, int headersState) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_TITLE, title);
+ args.putInt(ARG_HEADERS_STATE, headersState);
+ return args;
+ }
+
+ /**
+ * Sets the brand color for the browse fragment. The brand color is used as
+ * the primary color for UI elements in the browse fragment. For example,
+ * the background color of the headers fragment uses the brand color.
+ *
+ * @param color The color to use as the brand color of the fragment.
+ */
+ public void setBrandColor(@ColorInt int color) {
+ mBrandColor = color;
+ mBrandColorSet = true;
+
+ if (mHeadersSupportFragment != null) {
+ mHeadersSupportFragment.setBackgroundColor(mBrandColor);
+ }
+ }
+
+ /**
+ * Returns the brand color for the browse fragment.
+ * The default is transparent.
+ */
+ @ColorInt
+ public int getBrandColor() {
+ return mBrandColor;
+ }
+
+ /**
+ * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
+ * DividerRow and PageRow.
+ */
+ private void updateWrapperPresenter() {
+ if (mAdapter == null) {
+ mAdapterPresenter = null;
+ return;
+ }
+ final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
+ if (adapterPresenter == null) {
+ throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
+ }
+ if (adapterPresenter == mAdapterPresenter) {
+ return;
+ }
+ mAdapterPresenter = adapterPresenter;
+
+ Presenter[] presenters = adapterPresenter.getPresenters();
+ final Presenter invisibleRowPresenter = new InvisibleRowPresenter();
+ final Presenter[] allPresenters = new Presenter[presenters.length + 1];
+ System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);
+ allPresenters[allPresenters.length - 1] = invisibleRowPresenter;
+ mAdapter.setPresenterSelector(new PresenterSelector() {
+ @Override
+ public Presenter getPresenter(Object item) {
+ Row row = (Row) item;
+ if (row.isRenderedAsRowView()) {
+ return adapterPresenter.getPresenter(item);
+ } else {
+ return invisibleRowPresenter;
+ }
+ }
+
+ @Override
+ public Presenter[] getPresenters() {
+ return allPresenters;
+ }
+ });
+ }
+
+ /**
+ * Sets the adapter containing the rows for the fragment.
+ *
+ * <p>The items referenced by the adapter must be be derived from
+ * {@link Row}. These rows will be used by the rows fragment and the headers
+ * fragment (if not disabled) to render the browse rows.
+ *
+ * @param adapter An ObjectAdapter for the browse rows. All items must
+ * derive from {@link Row}.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ updateWrapperPresenter();
+ if (getView() == null) {
+ return;
+ }
+
+ updateMainFragmentRowsAdapter();
+ mHeadersSupportFragment.setAdapter(mAdapter);
+ }
+
+ void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+ if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+ return;
+ }
+ // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter
+ if (mMainFragmentRowsAdapter != null) {
+ // RowsFragment cannot change click/select listeners after view created.
+ // The main fragment and adapter should be GCed as long as there is no reference from
+ // BrowseSupportFragment to it.
+ mMainFragmentRowsAdapter.setAdapter(null);
+ }
+ mMainFragmentRowsAdapter = mainFragmentRowsAdapter;
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
+ new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
+ mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter
+ updateMainFragmentRowsAdapter();
+ }
+
+ /**
+ * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.
+ * It also clears old mMainFragmentListRowDataAdapter.
+ */
+ void updateMainFragmentRowsAdapter() {
+ if (mMainFragmentListRowDataAdapter != null) {
+ mMainFragmentListRowDataAdapter.detach();
+ mMainFragmentListRowDataAdapter = null;
+ }
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentListRowDataAdapter = mAdapter == null
+ ? null : new ListRowDataAdapter(mAdapter);
+ mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);
+ }
+ }
+
+ public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
+ return mMainFragmentAdapterRegistry;
+ }
+
+ /**
+ * Returns the adapter containing the rows for the fragment.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mExternalOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Returns an item selection listener.
+ */
+ public OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mExternalOnItemViewSelectedListener;
+ }
+
+ /**
+ * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has
+ * not been created yet or a different fragment is bound to it.
+ *
+ * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise.
+ */
+ public RowsSupportFragment getRowsSupportFragment() {
+ if (mMainFragment instanceof RowsSupportFragment) {
+ return (RowsSupportFragment) mMainFragment;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Current main fragment or null if not created.
+ */
+ public Fragment getMainFragment() {
+ return mMainFragment;
+ }
+
+ /**
+ * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
+ * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
+ */
+ public HeadersSupportFragment getHeadersSupportFragment() {
+ return mHeadersSupportFragment;
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
+ }
+ }
+
+ /**
+ * Returns the item Clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
+ * Starts a headers transition.
+ *
+ * <p>This method will begin a transition to either show or hide the
+ * headers, depending on the value of withHeaders. If headers are disabled
+ * for this browse fragment, this method will throw an exception.
+ *
+ * @param withHeaders True if the headers should transition to being shown,
+ * false if the transition should result in headers being hidden.
+ */
+ public void startHeadersTransition(boolean withHeaders) {
+ if (!mCanShowHeaders) {
+ throw new IllegalStateException("Cannot start headers transition");
+ }
+ if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
+ return;
+ }
+ startHeadersTransitionInternal(withHeaders);
+ }
+
+ /**
+ * Returns true if the headers transition is currently running.
+ */
+ public boolean isInHeadersTransition() {
+ return mHeadersTransition != null;
+ }
+
+ /**
+ * Returns true if headers are shown.
+ */
+ public boolean isShowingHeaders() {
+ return mShowingHeaders;
+ }
+
+ /**
+ * Sets a listener for browse fragment transitions.
+ *
+ * @param listener The listener to call when a browse headers transition
+ * begins or ends.
+ */
+ public void setBrowseTransitionListener(BrowseTransitionListener listener) {
+ mBrowseTransitionListener = listener;
+ }
+
+ /**
+ * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead.
+ *
+ * @param enable true to enable row scaling
+ */
+ @Deprecated
+ public void enableRowScaling(boolean enable) {
+ enableMainFragmentScaling(enable);
+ }
+
+ /**
+ * Enables scaling of main fragment when headers are present. For the page/row fragment,
+ * scaling is enabled only when both this method and
+ * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
+ *
+ * @param enable true to enable row scaling
+ */
+ public void enableMainFragmentScaling(boolean enable) {
+ mMainFragmentScaleEnabled = enable;
+ }
+
+ void startHeadersTransitionInternal(final boolean withHeaders) {
+ if (getFragmentManager().isDestroyed()) {
+ return;
+ }
+ if (!isHeadersDataReady()) {
+ return;
+ }
+ mShowingHeaders = withHeaders;
+ mMainFragmentAdapter.onTransitionPrepare();
+ mMainFragmentAdapter.onTransitionStart();
+ onExpandTransitionStart(!withHeaders, new Runnable() {
+ @Override
+ public void run() {
+ mHeadersSupportFragment.onTransitionPrepare();
+ mHeadersSupportFragment.onTransitionStart();
+ createHeadersTransition();
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
+ }
+ TransitionHelper.runTransition(
+ withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
+ if (mHeadersBackStackEnabled) {
+ if (!withHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ } else {
+ int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
+ if (index >= 0) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
+ getFragmentManager().popBackStackImmediate(entry.getId(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ boolean isVerticalScrolling() {
+ // don't run transition
+ return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
+ }
+
+
+ private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
+ new BrowseFrameLayout.OnFocusSearchListener() {
+ @Override
+ public View onFocusSearch(View focused, int direction) {
+ // if headers is running transition, focus stays
+ if (mCanShowHeaders && isInHeadersTransition()) {
+ return focused;
+ }
+ if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
+
+ if (getTitleView() != null && focused != getTitleView()
+ && direction == View.FOCUS_UP) {
+ return getTitleView();
+ }
+ if (getTitleView() != null && getTitleView().hasFocus()
+ && direction == View.FOCUS_DOWN) {
+ return mCanShowHeaders && mShowingHeaders
+ ? mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView();
+ }
+
+ boolean isRtl = ViewCompat.getLayoutDirection(focused)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
+ if (mCanShowHeaders && direction == towardStart) {
+ if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {
+ return focused;
+ }
+ return mHeadersSupportFragment.getVerticalGridView();
+ } else if (direction == towardEnd) {
+ if (isVerticalScrolling()) {
+ return focused;
+ } else if (mMainFragment != null && mMainFragment.getView() != null) {
+ return mMainFragment.getView();
+ }
+ return focused;
+ } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {
+ // disable focus_down moving into PageFragment.
+ return focused;
+ } else {
+ return null;
+ }
+ }
+ };
+
+ final boolean isHeadersDataReady() {
+ return mAdapter != null && mAdapter.size() != 0;
+ }
+
+ private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
+ new BrowseFrameLayout.OnChildFocusListener() {
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (getChildFragmentManager().isDestroyed()) {
+ return true;
+ }
+ // Make sure not changing focus when requestFocus() is called.
+ if (mCanShowHeaders && mShowingHeaders) {
+ if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null
+ && mHeadersSupportFragment.getView().requestFocus(
+ direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ if (mMainFragment != null && mMainFragment.getView() != null
+ && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ return getTitleView() != null
+ && getTitleView().requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public void onRequestChildFocus(View child, View focused) {
+ if (getChildFragmentManager().isDestroyed()) {
+ return;
+ }
+ if (!mCanShowHeaders || isInHeadersTransition()) return;
+ int childId = child.getId();
+ if (childId == R.id.browse_container_dock && mShowingHeaders) {
+ startHeadersTransitionInternal(false);
+ } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ };
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
+ outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
+
+ if (mBackStackChangedListener != null) {
+ mBackStackChangedListener.save(outState);
+ } else {
+ outState.putBoolean(HEADER_SHOW, mShowingHeaders);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final Context context = getContext();
+ TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
+ mContainerListMarginStart = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
+ .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
+ mContainerListAlignTop = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
+ .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
+ ta.recycle();
+
+ readArguments(getArguments());
+
+ if (mCanShowHeaders) {
+ if (mHeadersBackStackEnabled) {
+ mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
+ mBackStackChangedListener = new BackStackListener();
+ getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
+ mBackStackChangedListener.load(savedInstanceState);
+ } else {
+ if (savedInstanceState != null) {
+ mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
+ }
+ }
+ }
+
+ mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
+ }
+
+ @Override
+ public void onDestroyView() {
+ setMainFragmentRowsAdapter(null);
+ mPageRow = null;
+ mMainFragmentAdapter = null;
+ mMainFragment = null;
+ mHeadersSupportFragment = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mBackStackChangedListener != null) {
+ getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Creates a new {@link HeadersSupportFragment} instance. Subclass of BrowseSupportFragment may override and
+ * return an instance of subclass of HeadersSupportFragment, e.g. when app wants to replace presenter
+ * to render HeaderItem.
+ *
+ * @return A new instance of {@link HeadersSupportFragment} or its subclass.
+ */
+ public HeadersSupportFragment onCreateHeadersSupportFragment() {
+ return new HeadersSupportFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
+ mHeadersSupportFragment = onCreateHeadersSupportFragment();
+
+ createMainFragment(mAdapter, mSelectedPosition);
+ FragmentTransaction ft = getChildFragmentManager().beginTransaction()
+ .replace(R.id.browse_headers_dock, mHeadersSupportFragment);
+
+ if (mMainFragment != null) {
+ ft.replace(R.id.scale_frame, mMainFragment);
+ } else {
+ // Empty adapter used to guard against lazy adapter loading. When this
+ // fragment is instantiated, mAdapter might not have the data or might not
+ // have been set. In either of those cases mFragmentAdapter will be null.
+ // This way we can maintain the invariant that mMainFragmentAdapter is never
+ // null and it avoids doing null checks all over the code.
+ mMainFragmentAdapter = new MainFragmentAdapter(null);
+ mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+ }
+
+ ft.commit();
+ } else {
+ mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()
+ .findFragmentById(R.id.browse_headers_dock);
+ mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
+
+ mIsPageRow = savedInstanceState != null
+ && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
+ // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is
+ // the case for restoring, later if setSelection() triggers a createMainFragment(),
+ // should not create fragment.
+
+ mSelectedPosition = savedInstanceState != null
+ ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
+
+ setMainFragmentAdapter();
+ }
+
+ mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
+ if (mHeaderPresenterSelector != null) {
+ mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
+ mHeadersSupportFragment.setAdapter(mAdapter);
+ mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
+ mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener);
+
+ View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
+
+ getProgressBarManager().setRootView((ViewGroup)root);
+
+ mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
+ mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
+ mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
+
+ installTitleView(inflater, mBrowseFrame, savedInstanceState);
+
+ mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
+ mScaleFrameLayout.setPivotX(0);
+ mScaleFrameLayout.setPivotY(mContainerListAlignTop);
+
+ if (mBrandColorSet) {
+ mHeadersSupportFragment.setBackgroundColor(mBrandColor);
+ }
+
+ mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showHeaders(true);
+ }
+ });
+ mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showHeaders(false);
+ }
+ });
+ mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ setEntranceTransitionEndState();
+ }
+ });
+
+ return root;
+ }
+
+ void createHeadersTransition() {
+ mHeadersTransition = TransitionHelper.loadTransition(getContext(),
+ mShowingHeaders
+ ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
+
+ TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ }
+ @Override
+ public void onTransitionEnd(Object transition) {
+ mHeadersTransition = null;
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.onTransitionEnd();
+ if (!mShowingHeaders && mMainFragment != null) {
+ View mainFragmentView = mMainFragment.getView();
+ if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
+ mainFragmentView.requestFocus();
+ }
+ }
+ }
+ if (mHeadersSupportFragment != null) {
+ mHeadersSupportFragment.onTransitionEnd();
+ if (mShowingHeaders) {
+ VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView();
+ if (headerGridView != null && !headerGridView.hasFocus()) {
+ headerGridView.requestFocus();
+ }
+ }
+ }
+
+ // Animate TitleView once header animation is complete.
+ updateTitleViewVisibility();
+
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
+ }
+ }
+ });
+ }
+
+ void updateTitleViewVisibility() {
+ if (!mShowingHeaders) {
+ boolean showTitleView;
+ if (mIsPageRow && mMainFragmentAdapter != null) {
+ // page fragment case:
+ showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
+ } else {
+ // regular row view case:
+ showTitleView = isFirstRowWithContent(mSelectedPosition);
+ }
+ if (showTitleView) {
+ showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);
+ } else {
+ showTitle(false);
+ }
+ } else {
+ // when HeaderFragment is showing, showBranding and showSearch are slightly different
+ boolean showBranding;
+ boolean showSearch;
+ if (mIsPageRow && mMainFragmentAdapter != null) {
+ showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
+ } else {
+ showBranding = isFirstRowWithContent(mSelectedPosition);
+ }
+ showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);
+ int flags = 0;
+ if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;
+ if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;
+ if (flags != 0) {
+ showTitle(flags);
+ } else {
+ showTitle(false);
+ }
+ }
+ }
+
+ boolean isFirstRowWithContentOrPageRow(int rowPosition) {
+ if (mAdapter == null || mAdapter.size() == 0) {
+ return true;
+ }
+ for (int i = 0; i < mAdapter.size(); i++) {
+ final Row row = (Row) mAdapter.get(i);
+ if (row.isRenderedAsRowView() || row instanceof PageRow) {
+ return rowPosition == i;
+ }
+ }
+ return true;
+ }
+
+ boolean isFirstRowWithContent(int rowPosition) {
+ if (mAdapter == null || mAdapter.size() == 0) {
+ return true;
+ }
+ for (int i = 0; i < mAdapter.size(); i++) {
+ final Row row = (Row) mAdapter.get(i);
+ if (row.isRenderedAsRowView()) {
+ return rowPosition == i;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the {@link PresenterSelector} used to render the row headers.
+ *
+ * @param headerPresenterSelector The PresenterSelector that will determine
+ * the Presenter for each row header.
+ */
+ public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
+ mHeaderPresenterSelector = headerPresenterSelector;
+ if (mHeadersSupportFragment != null) {
+ mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
+ }
+
+ private void setHeadersOnScreen(boolean onScreen) {
+ MarginLayoutParams lp;
+ View containerList;
+ containerList = mHeadersSupportFragment.getView();
+ lp = (MarginLayoutParams) containerList.getLayoutParams();
+ lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
+ containerList.setLayoutParams(lp);
+ }
+
+ void showHeaders(boolean show) {
+ if (DEBUG) Log.v(TAG, "showHeaders " + show);
+ mHeadersSupportFragment.setHeadersEnabled(show);
+ setHeadersOnScreen(show);
+ expandMainFragment(!show);
+ }
+
+ private void expandMainFragment(boolean expand) {
+ MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
+ params.setMarginStart(!expand ? mContainerListMarginStart : 0);
+ mScaleFrameLayout.setLayoutParams(params);
+ mMainFragmentAdapter.setExpand(expand);
+
+ setMainFragmentAlignment();
+ final float scaleFactor = !expand
+ && mMainFragmentScaleEnabled
+ && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
+ mScaleFrameLayout.setLayoutScaleY(scaleFactor);
+ mScaleFrameLayout.setChildScale(scaleFactor);
+ }
+
+ private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener =
+ new HeadersSupportFragment.OnHeaderClickedListener() {
+ @Override
+ public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
+ if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
+ return;
+ }
+ startHeadersTransitionInternal(false);
+ mMainFragment.getView().requestFocus();
+ }
+ };
+
+ class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+
+ public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
+ mMainFragmentRowsAdapter = fragmentRowsAdapter;
+ }
+
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ int position = mMainFragmentRowsAdapter.getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "row selected position " + position);
+ onRowSelected(position);
+ if (mExternalOnItemViewSelectedListener != null) {
+ mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
+ private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
+ new HeadersSupportFragment.OnHeaderViewSelectedListener() {
+ @Override
+ public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
+ int position = mHeadersSupportFragment.getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "header selected position " + position);
+ onRowSelected(position);
+ }
+ };
+
+ void onRowSelected(int position) {
+ // even position is same, it could be data changed, always post selection runnable
+ // to possibly swap main fragment.
+ mSetSelectionRunnable.post(
+ position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
+ }
+
+ void setSelection(int position, boolean smooth) {
+ if (position == NO_POSITION) {
+ return;
+ }
+
+ mSelectedPosition = position;
+ if (mHeadersSupportFragment == null || mMainFragmentAdapter == null) {
+ // onDestroyView() called
+ return;
+ }
+ mHeadersSupportFragment.setSelectedPosition(position, smooth);
+ replaceMainFragment(position);
+
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
+ }
+
+ updateTitleViewVisibility();
+ }
+
+ private void replaceMainFragment(int position) {
+ if (createMainFragment(mAdapter, position)) {
+ swapToMainFragment();
+ expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
+ }
+ }
+
+ private void swapToMainFragment() {
+ final VerticalGridView gridView = mHeadersSupportFragment.getVerticalGridView();
+ if (isShowingHeaders() && gridView != null
+ && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
+ // if user is scrolling HeadersSupportFragment, swap to empty fragment and wait scrolling
+ // finishes.
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.scale_frame, new Fragment()).commit();
+ gridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ gridView.removeOnScrollListener(this);
+ FragmentManager fm = getChildFragmentManager();
+ Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);
+ if (currentFragment != mMainFragment) {
+ fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit();
+ }
+ }
+ }
+ });
+ } else {
+ // Otherwise swap immediately
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.scale_frame, mMainFragment).commit();
+ }
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Gets position of currently selected row.
+ * @return Position of currently selected row.
+ */
+ public int getSelectedPosition() {
+ return mSelectedPosition;
+ }
+
+ /**
+ * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.
+ */
+ public RowPresenter.ViewHolder getSelectedRowViewHolder() {
+ if (mMainFragmentRowsAdapter != null) {
+ int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();
+ return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ mSetSelectionRunnable.post(
+ position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
+ }
+
+ /**
+ * Selects a Row and perform an optional task on the Row. For example
+ * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
+ * scrolls to 11th row and selects 6th item on that row. The method will be ignored if
+ * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
+ * ViewGroup, Bundle)}).
+ *
+ * @param rowPosition Which row to select.
+ * @param smooth True to scroll to the row, false for no animation.
+ * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers
+ * fragment will be collapsed.
+ */
+ public void setSelectedPosition(int rowPosition, boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ if (mMainFragmentAdapterRegistry == null) {
+ return;
+ }
+ if (rowHolderTask != null) {
+ startHeadersTransition(false);
+ }
+ if (mMainFragmentRowsAdapter != null) {
+ mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHeadersSupportFragment.setAlignment(mContainerListAlignTop);
+ setMainFragmentAlignment();
+
+ if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment != null
+ && mHeadersSupportFragment.getView() != null) {
+ mHeadersSupportFragment.getView().requestFocus();
+ } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null
+ && mMainFragment.getView() != null) {
+ mMainFragment.getView().requestFocus();
+ }
+
+ if (mCanShowHeaders) {
+ showHeaders(mShowingHeaders);
+ }
+
+ mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
+ }
+
+ private void onExpandTransitionStart(boolean expand, final Runnable callback) {
+ if (expand) {
+ callback.run();
+ return;
+ }
+ // Run a "pre" layout when we go non-expand, in order to get the initial
+ // positions of added rows.
+ new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
+ }
+
+ private void setMainFragmentAlignment() {
+ int alignOffset = mContainerListAlignTop;
+ if (mMainFragmentScaleEnabled
+ && mMainFragmentAdapter.isScalingEnabled()
+ && mShowingHeaders) {
+ alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
+ }
+ mMainFragmentAdapter.setAlignment(alignOffset);
+ }
+
+ /**
+ * Enables/disables headers transition on back key support. This is enabled by
+ * default. The BrowseSupportFragment will add a back stack entry when headers are
+ * showing. Running a headers transition when the back key is pressed only
+ * works when the headers state is {@link #HEADERS_ENABLED} or
+ * {@link #HEADERS_HIDDEN}.
+ * <p>
+ * NOTE: If an Activity has its own onBackPressed() handling, you must
+ * disable this feature. You may use {@link #startHeadersTransition(boolean)}
+ * and {@link BrowseTransitionListener} in your own back stack handling.
+ */
+ public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
+ mHeadersBackStackEnabled = headersBackStackEnabled;
+ }
+
+ /**
+ * Returns true if headers transition on back key support is enabled.
+ */
+ public final boolean isHeadersTransitionOnBackEnabled() {
+ return mHeadersBackStackEnabled;
+ }
+
+ private void readArguments(Bundle args) {
+ if (args == null) {
+ return;
+ }
+ if (args.containsKey(ARG_TITLE)) {
+ setTitle(args.getString(ARG_TITLE));
+ }
+ if (args.containsKey(ARG_HEADERS_STATE)) {
+ setHeadersState(args.getInt(ARG_HEADERS_STATE));
+ }
+ }
+
+ /**
+ * Sets the state for the headers column in the browse fragment. Must be one
+ * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
+ * {@link #HEADERS_DISABLED}.
+ *
+ * @param headersState The state of the headers for the browse fragment.
+ */
+ public void setHeadersState(int headersState) {
+ if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
+ throw new IllegalArgumentException("Invalid headers state: " + headersState);
+ }
+ if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
+
+ if (headersState != mHeadersState) {
+ mHeadersState = headersState;
+ switch (headersState) {
+ case HEADERS_ENABLED:
+ mCanShowHeaders = true;
+ mShowingHeaders = true;
+ break;
+ case HEADERS_HIDDEN:
+ mCanShowHeaders = true;
+ mShowingHeaders = false;
+ break;
+ case HEADERS_DISABLED:
+ mCanShowHeaders = false;
+ mShowingHeaders = false;
+ break;
+ default:
+ Log.w(TAG, "Unknown headers state: " + headersState);
+ break;
+ }
+ if (mHeadersSupportFragment != null) {
+ mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
+ }
+ }
+ }
+
+ /**
+ * Returns the state of the headers column in the browse fragment.
+ */
+ public int getHeadersState() {
+ return mHeadersState;
+ }
+
+ @Override
+ protected Object createEntranceTransition() {
+ return TransitionHelper.loadTransition(getContext(),
+ R.transition.lb_browse_entrance_transition);
+ }
+
+ @Override
+ protected void runEntranceTransition(Object entranceTransition) {
+ TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
+ }
+
+ @Override
+ protected void onEntranceTransitionPrepare() {
+ mHeadersSupportFragment.onTransitionPrepare();
+ mMainFragmentAdapter.setEntranceTransitionState(false);
+ mMainFragmentAdapter.onTransitionPrepare();
+ }
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ mHeadersSupportFragment.onTransitionStart();
+ mMainFragmentAdapter.onTransitionStart();
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.onTransitionEnd();
+ }
+
+ if (mHeadersSupportFragment != null) {
+ mHeadersSupportFragment.onTransitionEnd();
+ }
+ }
+
+ void setSearchOrbViewOnScreen(boolean onScreen) {
+ View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();
+ if (searchOrbView != null) {
+ MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
+ lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
+ searchOrbView.setLayoutParams(lp);
+ }
+ }
+
+ void setEntranceTransitionStartState() {
+ setHeadersOnScreen(false);
+ setSearchOrbViewOnScreen(false);
+ // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+ // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+ // one when setEntranceTransitionStartState() is called.
+ }
+
+ void setEntranceTransitionEndState() {
+ setHeadersOnScreen(mShowingHeaders);
+ setSearchOrbViewOnScreen(true);
+ mMainFragmentAdapter.setEntranceTransitionState(true);
+ }
+
+ private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
+
+ private final View mView;
+ private final Runnable mCallback;
+ private int mState;
+ private MainFragmentAdapter mainFragmentAdapter;
+
+ final static int STATE_INIT = 0;
+ final static int STATE_FIRST_DRAW = 1;
+ final static int STATE_SECOND_DRAW = 2;
+
+ ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
+ mView = view;
+ mCallback = callback;
+ mainFragmentAdapter = adapter;
+ }
+
+ void execute() {
+ mView.getViewTreeObserver().addOnPreDrawListener(this);
+ mainFragmentAdapter.setExpand(false);
+ // always trigger onPreDraw even adapter setExpand() does nothing.
+ mView.invalidate();
+ mState = STATE_INIT;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (getView() == null || getContext() == null) {
+ mView.getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ if (mState == STATE_INIT) {
+ mainFragmentAdapter.setExpand(true);
+ // always trigger onPreDraw even adapter setExpand() does nothing.
+ mView.invalidate();
+ mState = STATE_FIRST_DRAW;
+ } else if (mState == STATE_FIRST_DRAW) {
+ mCallback.run();
+ mView.getViewTreeObserver().removeOnPreDrawListener(this);
+ mState = STATE_SECOND_DRAW;
+ }
+ return false;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
rename to leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
diff --git a/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
new file mode 100644
index 0000000..18934f4
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -0,0 +1,934 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsSupportFragment.java. DO NOT MODIFY. */
+
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
+import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
+import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.DetailsParallax;
+import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.ItemAlignmentFacet;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A fragment for creating Leanback details screens.
+ *
+ * <p>
+ * A DetailsFragment renders the elements of its {@link ObjectAdapter} as a set
+ * of rows in a vertical list.The Adapter's {@link PresenterSelector} must maintain subclasses
+ * of {@link RowPresenter}.
+ * </p>
+ *
+ * When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsFragment will
+ * setup default behavior of the DetailsOverviewRow:
+ * <li>
+ * The alignment of FullWidthDetailsOverviewRowPresenter is setup in
+ * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}.
+ * </li>
+ * <li>
+ * The view status switching of FullWidthDetailsOverviewRowPresenter is done in
+ * {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
+ * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}.
+ * </li>
+ *
+ * <p>
+ * The recommended activity themes to use with a DetailsFragment are
+ * <li>
+ * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details} with activity
+ * shared element transition for {@link FullWidthDetailsOverviewRowPresenter}.
+ * </li>
+ * <li>
+ * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details_NoSharedElementTransition}
+ * if shared element transition is not needed, for example if first row is not rendered by
+ * {@link FullWidthDetailsOverviewRowPresenter}.
+ * </li>
+ * </p>
+ *
+ * <p>
+ * DetailsFragment can use {@link DetailsFragmentBackgroundController} to add a parallax drawable
+ * background and embedded video playing fragment.
+ * </p>
+ * @deprecated use {@link DetailsSupportFragment}
+ */
+@Deprecated
+public class DetailsFragment extends BaseFragment {
+ static final String TAG = "DetailsFragment";
+ static boolean DEBUG = false;
+
+ final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ mRowsFragment.setEntranceTransitionState(false);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
+
+ void switchToVideoBeforeVideoFragmentCreated() {
+ // if the video fragment is not ready: immediately fade out covering drawable,
+ // hide title and mark mPendingFocusOnVideo and set focus on it later.
+ mDetailsBackgroundController.switchToVideoBeforeCreate();
+ showTitle(false);
+ mPendingFocusOnVideo = true;
+ slideOutGridView();
+ }
+
+ final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
+ false, false) {
+ @Override
+ public void run() {
+ switchToVideoBeforeVideoFragmentCreated();
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
+ false, false) {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout != null) {
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ // clear the activity enter/sharedElement transition, return transitions are kept.
+ // keep the return transitions and clear enter transition
+ if (getActivity() != null) {
+ Window window = getActivity().getWindow();
+ Object returnTransition = TransitionHelper.getReturnTransition(window);
+ Object sharedReturnTransition = TransitionHelper
+ .getSharedElementReturnTransition(window);
+ TransitionHelper.setEnterTransition(window, null);
+ TransitionHelper.setSharedElementEnterTransition(window, null);
+ TransitionHelper.setReturnTransition(window, returnTransition);
+ TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
+ }
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
+ true, false);
+
+ final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
+ TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout == null) {
+ new WaitEnterTransitionTimeout(DetailsFragment.this);
+ }
+ }
+ };
+
+ /**
+ * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+ * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
+ */
+ static class WaitEnterTransitionTimeout implements Runnable {
+ static final long WAIT_ENTERTRANSITION_START = 200;
+
+ final WeakReference<DetailsFragment> mRef;
+
+ WaitEnterTransitionTimeout(DetailsFragment f) {
+ mRef = new WeakReference<>(f);
+ f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+ }
+
+ @Override
+ public void run() {
+ DetailsFragment f = mRef.get();
+ if (f != null) {
+ f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
+ }
+ }
+ }
+
+ final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
+ @Override
+ public void run() {
+ onSafeStart();
+ }
+ };
+
+ final Event EVT_ONSTART = new Event("onStart");
+
+ final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
+
+ final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
+
+ final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
+
+ final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ mStateMachine.addState(STATE_ON_SAFE_START);
+ mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ /**
+ * Part 1: Processing enter transitions after fragment.onCreate
+ */
+ mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
+ // if transition is not supported, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ // if transition is not set on Activity, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_NO_ENTER_TRANSITION);
+ // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
+ // complete.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
+ // once after onCreateView, we cannot skip the enter transition, add a listener and wait
+ // it to finish
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
+ EVT_ON_CREATEVIEW);
+ // when enter transition finishes, go to complete, however this might never happen if
+ // the activity is not giving transition options in startActivity, there is no API to query
+ // if this activity is started in a enter transition mode. So we rely on a timer below:
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
+ // we are expecting app to start delayed enter transition shortly after details row is
+ // loaded, so create a timer and wait for enter transition start.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
+ // if enter transition not started in the timer, skip to DONE, this can be also true when
+ // startActivity is not giving transition option.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_ENTER_TRANSIITON_DONE);
+
+ /**
+ * Part 2: modification to the entrance transition defined in BaseFragment
+ */
+ // Must finish enter transition before perform entrance transition.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
+ // Calling switch to video would hide immediately and skip entrance transition
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
+ // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
+ // still need to do the switchToVideo.
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+
+ // for once the view is created in onStart and prepareEntranceTransition was called, we
+ // could setEntranceStartState:
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
+
+ /**
+ * Part 3: onSafeStart()
+ */
+ // for onSafeStart: the condition is onStart called, entrance transition complete
+ mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
+ }
+
+ private class SetSelectionRunnable implements Runnable {
+ int mPosition;
+ boolean mSmooth = true;
+
+ SetSelectionRunnable() {
+ }
+
+ @Override
+ public void run() {
+ if (mRowsFragment == null) {
+ return;
+ }
+ mRowsFragment.setSelectedPosition(mPosition, mSmooth);
+ }
+ }
+
+ TransitionListener mEnterTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ if (mWaitEnterTransitionTimeout != null) {
+ // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
+ // when transition finishes.
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Object transition) {
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
+ }
+
+ @Override
+ public void onTransitionEnd(Object transition) {
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
+ }
+ };
+
+ TransitionListener mReturnTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ onReturnTransitionStart();
+ }
+ };
+
+ BrowseFrameLayout mRootView;
+ View mBackgroundView;
+ Drawable mBackgroundDrawable;
+ Fragment mVideoFragment;
+ DetailsParallax mDetailsParallax;
+ RowsFragment mRowsFragment;
+ ObjectAdapter mAdapter;
+ int mContainerListAlignTop;
+ BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener;
+ BaseOnItemViewClickedListener mOnItemViewClickedListener;
+ DetailsFragmentBackgroundController mDetailsBackgroundController;
+
+ // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
+ // true, we will focus to VideoFragment immediately after video fragment's view is created.
+ boolean mPendingFocusOnVideo = false;
+
+ WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
+
+ Object mSceneAfterEntranceTransition;
+
+ final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
+
+ final BaseOnItemViewSelectedListener<Object> mOnItemViewSelectedListener =
+ new BaseOnItemViewSelectedListener<Object>() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Object row) {
+ int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
+ int subposition = mRowsFragment.getVerticalGridView().getSelectedSubPosition();
+ if (DEBUG) Log.v(TAG, "row selected position " + position
+ + " subposition " + subposition);
+ onRowSelected(position, subposition);
+ if (mExternalOnItemViewSelectedListener != null) {
+ mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
+ /**
+ * Sets the list of rows for the fragment.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ Presenter[] presenters = adapter.getPresenterSelector().getPresenters();
+ if (presenters != null) {
+ for (int i = 0; i < presenters.length; i++) {
+ setupPresenter(presenters[i]);
+ }
+ } else {
+ Log.e(TAG, "PresenterSelector.getPresenters() not implemented");
+ }
+ if (mRowsFragment != null) {
+ mRowsFragment.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Returns the list of rows.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
+ mExternalOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item clicked listener.
+ */
+ public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
+ if (mOnItemViewClickedListener != listener) {
+ mOnItemViewClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemViewClickedListener(listener);
+ }
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mContainerListAlignTop =
+ getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
+ if (transition == null) {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
+ }
+ transition = TransitionHelper.getReturnTransition(activity.getWindow());
+ if (transition != null) {
+ TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
+ }
+ } else {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRootView = (BrowseFrameLayout) inflater.inflate(
+ R.layout.lb_details_fragment, container, false);
+ mBackgroundView = mRootView.findViewById(R.id.details_background_view);
+ if (mBackgroundView != null) {
+ mBackgroundView.setBackground(mBackgroundDrawable);
+ }
+ mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
+ R.id.details_rows_dock);
+ if (mRowsFragment == null) {
+ mRowsFragment = new RowsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.details_rows_dock, mRowsFragment).commit();
+ }
+ installTitleView(inflater, mRootView, savedInstanceState);
+ mRowsFragment.setAdapter(mAdapter);
+ mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
+
+ mSceneAfterEntranceTransition = TransitionHelper.createScene(mRootView, new Runnable() {
+ @Override
+ public void run() {
+ mRowsFragment.setEntranceTransitionState(true);
+ }
+ });
+
+ setupDpadNavigation();
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Setup adapter listener to work with ParallaxTransition (>= API 21).
+ mRowsFragment.setExternalAdapterListener(new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+ if (mDetailsParallax != null && vh.getViewHolder()
+ instanceof FullWidthDetailsOverviewRowPresenter.ViewHolder) {
+ FullWidthDetailsOverviewRowPresenter.ViewHolder rowVh =
+ (FullWidthDetailsOverviewRowPresenter.ViewHolder)
+ vh.getViewHolder();
+ rowVh.getOverviewView().setTag(R.id.lb_parallax_source,
+ mDetailsParallax);
+ }
+ }
+ });
+ }
+ return mRootView;
+ }
+
+ /**
+ * @deprecated override {@link #onInflateTitleView(LayoutInflater,ViewGroup,Bundle)} instead.
+ */
+ @Deprecated
+ protected View inflateTitle(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ return super.onInflateTitleView(inflater, parent, savedInstanceState);
+ }
+
+ @Override
+ public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ return inflateTitle(inflater, parent, savedInstanceState);
+ }
+
+ void setVerticalGridViewLayout(VerticalGridView listview) {
+ // align the top edge of item to a fixed position
+ listview.setItemAlignmentOffset(-mContainerListAlignTop);
+ listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ listview.setWindowAlignmentOffset(0);
+ listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+
+ /**
+ * Called to setup each Presenter of Adapter passed in {@link #setAdapter(ObjectAdapter)}.Note
+ * that setup should only change the Presenter behavior that is meaningful in DetailsFragment.
+ * For example how a row is aligned in details Fragment. The default implementation invokes
+ * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}
+ *
+ */
+ protected void setupPresenter(Presenter rowPresenter) {
+ if (rowPresenter instanceof FullWidthDetailsOverviewRowPresenter) {
+ setupDetailsOverviewRowPresenter((FullWidthDetailsOverviewRowPresenter) rowPresenter);
+ }
+ }
+
+ /**
+ * Called to setup {@link FullWidthDetailsOverviewRowPresenter}. The default implementation
+ * adds two alignment positions({@link ItemAlignmentFacet}) for ViewHolder of
+ * FullWidthDetailsOverviewRowPresenter to align in fragment.
+ */
+ protected void setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter presenter) {
+ ItemAlignmentFacet facet = new ItemAlignmentFacet();
+ // by default align details_frame to half window height
+ ItemAlignmentFacet.ItemAlignmentDef alignDef1 = new ItemAlignmentFacet.ItemAlignmentDef();
+ alignDef1.setItemAlignmentViewId(R.id.details_frame);
+ alignDef1.setItemAlignmentOffset(- getResources()
+ .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions));
+ alignDef1.setItemAlignmentOffsetPercent(0);
+ // when description is selected, align details_frame to top edge
+ ItemAlignmentFacet.ItemAlignmentDef alignDef2 = new ItemAlignmentFacet.ItemAlignmentDef();
+ alignDef2.setItemAlignmentViewId(R.id.details_frame);
+ alignDef2.setItemAlignmentFocusViewId(R.id.details_overview_description);
+ alignDef2.setItemAlignmentOffset(- getResources()
+ .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description));
+ alignDef2.setItemAlignmentOffsetPercent(0);
+ ItemAlignmentFacet.ItemAlignmentDef[] defs =
+ new ItemAlignmentFacet.ItemAlignmentDef[] {alignDef1, alignDef2};
+ facet.setAlignmentDefs(defs);
+ presenter.setFacet(ItemAlignmentFacet.class, facet);
+ }
+
+ VerticalGridView getVerticalGridView() {
+ return mRowsFragment == null ? null : mRowsFragment.getVerticalGridView();
+ }
+
+ /**
+ * Gets embedded RowsFragment showing multiple rows for DetailsFragment. If view of
+ * DetailsFragment is not created, the method returns null.
+ * @return Embedded RowsFragment showing multiple rows for DetailsFragment.
+ */
+ public RowsFragment getRowsFragment() {
+ return mRowsFragment;
+ }
+
+ /**
+ * Setup dimensions that are only meaningful when the child Fragments are inside
+ * DetailsFragment.
+ */
+ private void setupChildFragmentLayout() {
+ setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ mSetSelectionRunnable.mPosition = position;
+ mSetSelectionRunnable.mSmooth = smooth;
+ if (getView() != null && getView().getHandler() != null) {
+ getView().getHandler().post(mSetSelectionRunnable);
+ }
+ }
+
+ void switchToVideo() {
+ if (mVideoFragment != null && mVideoFragment.getView() != null) {
+ mVideoFragment.getView().requestFocus();
+ } else {
+ mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
+ }
+ }
+
+ void switchToRows() {
+ mPendingFocusOnVideo = false;
+ VerticalGridView verticalGridView = getVerticalGridView();
+ if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
+ verticalGridView.requestFocus();
+ }
+ }
+
+ /**
+ * This method asks DetailsFragmentBackgroundController to add a fragment for rendering video.
+ * In case the fragment is already there, it will return the existing one. The method must be
+ * called after calling super.onCreate(). App usually does not call this method directly.
+ *
+ * @return Fragment the added or restored fragment responsible for rendering video.
+ * @see DetailsFragmentBackgroundController#onCreateVideoFragment()
+ */
+ final Fragment findOrCreateVideoFragment() {
+ if (mVideoFragment != null) {
+ return mVideoFragment;
+ }
+ Fragment fragment = getChildFragmentManager()
+ .findFragmentById(R.id.video_surface_container);
+ if (fragment == null && mDetailsBackgroundController != null) {
+ FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+ ft2.add(android.support.v17.leanback.R.id.video_surface_container,
+ fragment = mDetailsBackgroundController.onCreateVideoFragment());
+ ft2.commit();
+ if (mPendingFocusOnVideo) {
+ // wait next cycle for Fragment view created so we can focus on it.
+ // This is a bit hack eventually we will do commitNow() which get view immediately.
+ getView().post(new Runnable() {
+ @Override
+ public void run() {
+ if (getView() != null) {
+ switchToVideo();
+ }
+ mPendingFocusOnVideo = false;
+ }
+ });
+ }
+ }
+ mVideoFragment = fragment;
+ return mVideoFragment;
+ }
+
+ void onRowSelected(int selectedPosition, int selectedSubPosition) {
+ ObjectAdapter adapter = getAdapter();
+ if (( mRowsFragment != null && mRowsFragment.getView() != null
+ && mRowsFragment.getView().hasFocus() && !mPendingFocusOnVideo)
+ && (adapter == null || adapter.size() == 0
+ || (getVerticalGridView().getSelectedPosition() == 0
+ && getVerticalGridView().getSelectedSubPosition() == 0))) {
+ showTitle(true);
+ } else {
+ showTitle(false);
+ }
+ if (adapter != null && adapter.size() > selectedPosition) {
+ final VerticalGridView gridView = getVerticalGridView();
+ final int count = gridView.getChildCount();
+ if (count > 0) {
+ mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
+ }
+ for (int i = 0; i < count; i++) {
+ ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
+ gridView.getChildViewHolder(gridView.getChildAt(i));
+ RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter();
+ onSetRowStatus(rowPresenter,
+ rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()),
+ bridgeViewHolder.getAdapterPosition(),
+ selectedPosition, selectedSubPosition);
+ }
+ }
+ }
+
+ /**
+ * Called when onStart and enter transition (postponed/none postponed) and entrance transition
+ * are all finished.
+ */
+ @CallSuper
+ void onSafeStart() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.onStart();
+ }
+ }
+
+ @CallSuper
+ void onReturnTransitionStart() {
+ if (mDetailsBackgroundController != null) {
+ // first disable parallax effect that auto-start PlaybackGlue.
+ boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
+ // if video is not visible we can safely remove VideoFragment,
+ // otherwise let video playing during return transition.
+ if (!isVideoVisible && mVideoFragment != null) {
+ FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+ ft2.remove(mVideoFragment);
+ ft2.commit();
+ mVideoFragment = null;
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.onStop();
+ }
+ super.onStop();
+ }
+
+ /**
+ * Called on every visible row to change view status when current selected row position
+ * or selected sub position changed. Subclass may override. The default
+ * implementation calls {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
+ * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)} if presenter is
+ * instance of {@link FullWidthDetailsOverviewRowPresenter}.
+ *
+ * @param presenter The presenter used to create row ViewHolder.
+ * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not
+ * be selected.
+ * @param adapterPosition The adapter position of viewHolder inside adapter.
+ * @param selectedPosition The adapter position of currently selected row.
+ * @param selectedSubPosition The sub position within currently selected row. This is used
+ * When a row has multiple alignment positions.
+ */
+ protected void onSetRowStatus(RowPresenter presenter, RowPresenter.ViewHolder viewHolder, int
+ adapterPosition, int selectedPosition, int selectedSubPosition) {
+ if (presenter instanceof FullWidthDetailsOverviewRowPresenter) {
+ onSetDetailsOverviewRowStatus((FullWidthDetailsOverviewRowPresenter) presenter,
+ (FullWidthDetailsOverviewRowPresenter.ViewHolder) viewHolder,
+ adapterPosition, selectedPosition, selectedSubPosition);
+ }
+ }
+
+ /**
+ * Called to change DetailsOverviewRow view status when current selected row position
+ * or selected sub position changed. Subclass may override. The default
+ * implementation switches between three states based on the positions:
+ * {@link FullWidthDetailsOverviewRowPresenter#STATE_HALF},
+ * {@link FullWidthDetailsOverviewRowPresenter#STATE_FULL} and
+ * {@link FullWidthDetailsOverviewRowPresenter#STATE_SMALL}.
+ *
+ * @param presenter The presenter used to create row ViewHolder.
+ * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not
+ * be selected.
+ * @param adapterPosition The adapter position of viewHolder inside adapter.
+ * @param selectedPosition The adapter position of currently selected row.
+ * @param selectedSubPosition The sub position within currently selected row. This is used
+ * When a row has multiple alignment positions.
+ */
+ protected void onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter presenter,
+ FullWidthDetailsOverviewRowPresenter.ViewHolder viewHolder, int adapterPosition,
+ int selectedPosition, int selectedSubPosition) {
+ if (selectedPosition > adapterPosition) {
+ presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
+ } else if (selectedPosition == adapterPosition && selectedSubPosition == 1) {
+ presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
+ } else if (selectedPosition == adapterPosition && selectedSubPosition == 0){
+ presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_FULL);
+ } else {
+ presenter.setState(viewHolder,
+ FullWidthDetailsOverviewRowPresenter.STATE_SMALL);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ setupChildFragmentLayout();
+ mStateMachine.fireEvent(EVT_ONSTART);
+ if (mDetailsParallax != null) {
+ mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
+ }
+ if (mPendingFocusOnVideo) {
+ slideOutGridView();
+ } else if (!getView().hasFocus()) {
+ mRowsFragment.getVerticalGridView().requestFocus();
+ }
+ }
+
+ @Override
+ protected Object createEntranceTransition() {
+ return TransitionHelper.loadTransition(FragmentUtil.getContext(DetailsFragment.this),
+ R.transition.lb_details_enter_transition);
+ }
+
+ @Override
+ protected void runEntranceTransition(Object entranceTransition) {
+ TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ mRowsFragment.onTransitionEnd();
+ }
+
+ @Override
+ protected void onEntranceTransitionPrepare() {
+ mRowsFragment.onTransitionPrepare();
+ }
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ mRowsFragment.onTransitionStart();
+ }
+
+ /**
+ * Returns the {@link DetailsParallax} instance used by
+ * {@link DetailsFragmentBackgroundController} to configure parallax effect of background and
+ * control embedded video playback. App usually does not use this method directly.
+ * App may use this method for other custom parallax tasks.
+ *
+ * @return The DetailsParallax instance attached to the DetailsFragment.
+ */
+ public DetailsParallax getParallax() {
+ if (mDetailsParallax == null) {
+ mDetailsParallax = new DetailsParallax();
+ if (mRowsFragment != null && mRowsFragment.getView() != null) {
+ mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
+ }
+ }
+ return mDetailsParallax;
+ }
+
+ /**
+ * Set background drawable shown below foreground rows UI and above
+ * {@link #findOrCreateVideoFragment()}.
+ *
+ * @see DetailsFragmentBackgroundController
+ */
+ void setBackgroundDrawable(Drawable drawable) {
+ if (mBackgroundView != null) {
+ mBackgroundView.setBackground(drawable);
+ }
+ mBackgroundDrawable = drawable;
+ }
+
+ /**
+ * This method does the following
+ * <ul>
+ * <li>sets up focus search handling logic in the root view to enable transitioning between
+ * half screen/full screen/no video mode.</li>
+ *
+ * <li>Sets up the key listener in the root view to intercept events like UP/DOWN and
+ * transition to appropriate mode like half/full screen video.</li>
+ * </ul>
+ */
+ void setupDpadNavigation() {
+ mRootView.setOnChildFocusListener(new BrowseFrameLayout.OnChildFocusListener() {
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ return false;
+ }
+
+ @Override
+ public void onRequestChildFocus(View child, View focused) {
+ if (child != mRootView.getFocusedChild()) {
+ if (child.getId() == R.id.details_fragment_root) {
+ if (!mPendingFocusOnVideo) {
+ slideInGridView();
+ showTitle(true);
+ }
+ } else if (child.getId() == R.id.video_surface_container) {
+ slideOutGridView();
+ showTitle(false);
+ } else {
+ showTitle(true);
+ }
+ }
+ }
+ });
+ mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
+ @Override
+ public View onFocusSearch(View focused, int direction) {
+ if (mRowsFragment.getVerticalGridView() != null
+ && mRowsFragment.getVerticalGridView().hasFocus()) {
+ if (direction == View.FOCUS_UP) {
+ if (mDetailsBackgroundController != null
+ && mDetailsBackgroundController.canNavigateToVideoFragment()
+ && mVideoFragment != null && mVideoFragment.getView() != null) {
+ return mVideoFragment.getView();
+ } else if (getTitleView() != null && getTitleView().hasFocusable()) {
+ return getTitleView();
+ }
+ }
+ } else if (getTitleView() != null && getTitleView().hasFocus()) {
+ if (direction == View.FOCUS_DOWN) {
+ if (mRowsFragment.getVerticalGridView() != null) {
+ return mRowsFragment.getVerticalGridView();
+ }
+ }
+ }
+ return focused;
+ }
+ });
+
+ // If we press BACK on remote while in full screen video mode, we should
+ // transition back to half screen video playback mode.
+ mRootView.setOnDispatchKeyListener(new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // This is used to check if we are in full screen video mode. This is somewhat
+ // hacky and relies on the behavior of the video helper class to update the
+ // focusability of the video surface view.
+ if (mVideoFragment != null && mVideoFragment.getView() != null
+ && mVideoFragment.getView().hasFocus()) {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ if (getVerticalGridView().getChildCount() > 0) {
+ getVerticalGridView().requestFocus();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Slides vertical grid view (displaying media item details) out of the screen from below.
+ */
+ void slideOutGridView() {
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().animateOut();
+ }
+ }
+
+ void slideInGridView() {
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().animateIn();
+ }
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
new file mode 100644
index 0000000..25ed723
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -0,0 +1,497 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.animation.PropertyValuesHolder;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+import android.app.Fragment;
+
+/**
+ * Controller for DetailsFragment parallax background and embedded video play.
+ * <p>
+ * The parallax background drawable is made of two parts: cover drawable (by default
+ * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default
+ * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size
+ * of cover drawable and bottom drawable will be updated and the cover drawable will by default
+ * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.
+ * </p>
+ * <pre>
+ * ***************************
+ * * Cover Drawable *
+ * * (FitWidthBitmapDrawable)*
+ * * *
+ * ***************************
+ * * DetailsOverviewRow *
+ * * *
+ * ***************************
+ * * Bottom Drawable *
+ * * (ColorDrawable) *
+ * * Related *
+ * * Content *
+ * ***************************
+ * </pre>
+ * Both parallax background drawable and embedded video play are optional. App must call
+ * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.
+ * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and
+ * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable
+ * will be faded out.
+ * Example:
+ * <pre>
+ * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(this);
+ *
+ * public void onCreate(Bundle savedInstance) {
+ * super.onCreate(savedInstance);
+ * MediaPlayerGlue player = new MediaPlayerGlue(..);
+ * player.setUrl(...);
+ * mController.enableParallax();
+ * mController.setupVideoPlayback(player);
+ * }
+ *
+ * static class MyLoadBitmapTask extends ... {
+ * WeakReference<MyFragment> mFragmentRef;
+ * MyLoadBitmapTask(MyFragment fragment) {
+ * mFragmentRef = new WeakReference(fragment);
+ * }
+ * protected void onPostExecute(Bitmap bitmap) {
+ * MyFragment fragment = mFragmentRef.get();
+ * if (fragment != null) {
+ * fragment.mController.setCoverBitmap(bitmap);
+ * }
+ * }
+ * }
+ *
+ * public void onStart() {
+ * new MyLoadBitmapTask(this).execute(url);
+ * }
+ *
+ * public void onStop() {
+ * mController.setCoverBitmap(null);
+ * }
+ * </pre>
+ * <p>
+ * To customize cover drawable and/or bottom drawable, app should call
+ * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.
+ * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.
+ * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.
+ * </p>
+ * <p>
+ * To customize playback fragment, app should override {@link #onCreateVideoFragment()} and
+ * {@link #onCreateGlueHost()}.
+ * </p>
+ *
+ * @deprecated use {@link DetailsSupportFragmentBackgroundController}
+ */
+@Deprecated
+public class DetailsFragmentBackgroundController {
+
+ final DetailsFragment mFragment;
+ DetailsParallaxDrawable mParallaxDrawable;
+ int mParallaxDrawableMaxOffset;
+ PlaybackGlue mPlaybackGlue;
+ DetailsBackgroundVideoHelper mVideoHelper;
+ Bitmap mCoverBitmap;
+ int mSolidColor;
+ boolean mCanUseHost = false;
+ boolean mInitialControlVisible = false;
+
+ private Fragment mLastVideoFragmentForGlueHost;
+
+ /**
+ * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that
+ * each DetailsFragment can only associate with one DetailsFragmentBackgroundController.
+ *
+ * @param fragment The DetailsFragment to control background and embedded video playing.
+ * @throws IllegalStateException If fragment was already associated with another controller.
+ */
+ public DetailsFragmentBackgroundController(DetailsFragment fragment) {
+ if (fragment.mDetailsBackgroundController != null) {
+ throw new IllegalStateException("Each DetailsFragment is allowed to initialize "
+ + "DetailsFragmentBackgroundController once");
+ }
+ fragment.mDetailsBackgroundController = this;
+ mFragment = fragment;
+ }
+
+ /**
+ * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable
+ * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied
+ * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and
+ * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.
+ * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+ *
+ * @see #setCoverBitmap(Bitmap)
+ * @see #setSolidColor(int)
+ * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+ */
+ public void enableParallax() {
+ int offset = mParallaxDrawableMaxOffset;
+ if (offset == 0) {
+ offset = FragmentUtil.getContext(mFragment).getResources()
+ .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);
+ }
+ Drawable coverDrawable = new FitWidthBitmapDrawable();
+ ColorDrawable colorDrawable = new ColorDrawable();
+ enableParallax(coverDrawable, colorDrawable,
+ new ParallaxTarget.PropertyValuesHolderTarget(
+ coverDrawable,
+ PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+ 0, -offset)
+ ));
+ }
+
+ /**
+ * Enables parallax background using a custom cover drawable at top and a custom bottom
+ * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
+ *
+ * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}
+ * will not work if coverDrawable is not {@link FitWidthBitmapDrawable};
+ * in that case it's app's responsibility to set content into
+ * coverDrawable.
+ * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work
+ * if bottomDrawable is not {@link ColorDrawable}; in that case it's app's
+ * responsibility to set content of bottomDrawable.
+ * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.
+ * Use null for no parallax movement effect.
+ * Example to move bitmap within FitWidthBitmapDrawable:
+ * new ParallaxTarget.PropertyValuesHolderTarget(
+ * coverDrawable, PropertyValuesHolder.ofInt(
+ * FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
+ * 0, -120))
+ * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
+ */
+ public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
+ @Nullable ParallaxTarget.PropertyValuesHolderTarget
+ coverDrawableParallaxTarget) {
+ if (mParallaxDrawable != null) {
+ return;
+ }
+ // if bitmap is set before enableParallax, use it as initial value.
+ if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {
+ ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);
+ }
+ // if solid color is set before enableParallax, use it as initial value.
+ if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {
+ ((ColorDrawable) bottomDrawable).setColor(mSolidColor);
+ }
+ if (mPlaybackGlue != null) {
+ throw new IllegalStateException("enableParallaxDrawable must be called before "
+ + "enableVideoPlayback");
+ }
+ mParallaxDrawable = new DetailsParallaxDrawable(
+ FragmentUtil.getContext(mFragment),
+ mFragment.getParallax(),
+ coverDrawable,
+ bottomDrawable,
+ coverDrawableParallaxTarget);
+ mFragment.setBackgroundDrawable(mParallaxDrawable);
+ // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
+ // before PlaybackGlue is ready.
+ mVideoHelper = new DetailsBackgroundVideoHelper(null,
+ mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
+ }
+
+ /**
+ * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
+ * creates a VideoFragment and VideoFragmentGlueHost to host the PlaybackGlue.
+ * This method must be called after calling details Fragment super.onCreate(). This method
+ * can be called multiple times to replace existing PlaybackGlue or calling
+ * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases
+ * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue}
+ * subclass is not doing that, it's app's responsibility to release the resources.
+ *
+ * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one.
+ * @see #onCreateVideoFragment()
+ * @see #onCreateGlueHost().
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
+ if (mPlaybackGlue == playbackGlue) {
+ return;
+ }
+
+ PlaybackGlueHost playbackGlueHost = null;
+ if (mPlaybackGlue != null) {
+ playbackGlueHost = mPlaybackGlue.getHost();
+ mPlaybackGlue.setHost(null);
+ }
+
+ mPlaybackGlue = playbackGlue;
+ mVideoHelper.setPlaybackGlue(mPlaybackGlue);
+ if (mCanUseHost && mPlaybackGlue != null) {
+ if (playbackGlueHost == null
+ || mLastVideoFragmentForGlueHost != findOrCreateVideoFragment()) {
+ mPlaybackGlue.setHost(createGlueHost());
+ mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
+ } else {
+ mPlaybackGlue.setHost(playbackGlueHost);
+ }
+ }
+ }
+
+ /**
+ * Returns current PlaybackGlue or null if not set or cleared.
+ *
+ * @return Current PlaybackGlue or null
+ */
+ public final PlaybackGlue getPlaybackGlue() {
+ return mPlaybackGlue;
+ }
+
+ /**
+ * Precondition allows user navigate to video fragment using DPAD. Default implementation
+ * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation
+ * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block
+ * app calls {@link #switchToVideo}.
+ *
+ * @return True allow to navigate to video fragment.
+ */
+ public boolean canNavigateToVideoFragment() {
+ return mPlaybackGlue != null;
+ }
+
+ void switchToVideoBeforeCreate() {
+ mVideoHelper.crossFadeBackgroundToVideo(true, true);
+ mInitialControlVisible = true;
+ }
+
+ /**
+ * Switch to video fragment, note that this method is not affected by result of
+ * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()
+ * it will make video fragment to be initially focused once it is created.
+ * <p>
+ * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter
+ * transition and shared element transition.
+ * </p>
+ * <p>
+ * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and
+ * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.
+ * </p>
+ * <p>
+ * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an
+ * IllegalStateException will be thrown.
+ * </p>
+ */
+ public final void switchToVideo() {
+ mFragment.switchToVideo();
+ }
+
+ /**
+ * Switch to rows fragment.
+ */
+ public final void switchToRows() {
+ mFragment.switchToRows();
+ }
+
+ /**
+ * When fragment is started and no running transition. First set host if not yet set, second
+ * start playing if it was paused before.
+ */
+ void onStart() {
+ if (!mCanUseHost) {
+ mCanUseHost = true;
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.setHost(createGlueHost());
+ mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
+ }
+ }
+ if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
+ mPlaybackGlue.play();
+ }
+ }
+
+ void onStop() {
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.pause();
+ }
+ }
+
+ /**
+ * Disable parallax that would auto-start video playback
+ * @return true if video fragment is visible or false otherwise.
+ */
+ boolean disableVideoParallax() {
+ if (mVideoHelper != null) {
+ mVideoHelper.stopParallax();
+ return mVideoHelper.isVideoVisible();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.
+ * By default it's a {@link FitWidthBitmapDrawable}.
+ *
+ * @return The cover drawable at top.
+ */
+ public final Drawable getCoverDrawable() {
+ if (mParallaxDrawable == null) {
+ return null;
+ }
+ return mParallaxDrawable.getCoverDrawable();
+ }
+
+ /**
+ * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.
+ * By default it's a {@link ColorDrawable}.
+ *
+ * @return The bottom drawable.
+ */
+ public final Drawable getBottomDrawable() {
+ if (mParallaxDrawable == null) {
+ return null;
+ }
+ return mParallaxDrawable.getBottomDrawable();
+ }
+
+ /**
+ * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoFragment} by
+ * default. App may override and return a different fragment and it also must override
+ * {@link #onCreateGlueHost()}.
+ *
+ * @return A new fragment used in {@link #onCreateGlueHost()}.
+ * @see #onCreateGlueHost()
+ * @see #setupVideoPlayback(PlaybackGlue)
+ */
+ public Fragment onCreateVideoFragment() {
+ return new VideoFragment();
+ }
+
+ /**
+ * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
+ * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment
+ * super.onCreate(). When override this method, app may call
+ * {@link #findOrCreateVideoFragment()} to get or create a fragment.
+ *
+ * @return A new PlaybackGlueHost to host PlaybackGlue.
+ * @see #onCreateVideoFragment()
+ * @see #findOrCreateVideoFragment()
+ * @see #setupVideoPlayback(PlaybackGlue)
+ */
+ public PlaybackGlueHost onCreateGlueHost() {
+ return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());
+ }
+
+ PlaybackGlueHost createGlueHost() {
+ PlaybackGlueHost host = onCreateGlueHost();
+ if (mInitialControlVisible) {
+ host.showControlsOverlay(false);
+ } else {
+ host.hideControlsOverlay(false);
+ }
+ return host;
+ }
+
+ /**
+ * Adds or gets fragment for rendering video in DetailsFragment. A subclass that
+ * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
+ * a {@link PlaybackGlueHost}.
+ *
+ * @return Fragment the added or restored fragment responsible for rendering video.
+ * @see #onCreateGlueHost()
+ */
+ public final Fragment findOrCreateVideoFragment() {
+ return mFragment.findOrCreateVideoFragment();
+ }
+
+ /**
+ * Convenient method to set Bitmap in cover drawable. If app is not using default
+ * {@link FitWidthBitmapDrawable}, app should not use this method It's safe to call
+ * setCoverBitmap() before calling {@link #enableParallax()}.
+ *
+ * @param bitmap bitmap to set as cover.
+ */
+ public final void setCoverBitmap(Bitmap bitmap) {
+ mCoverBitmap = bitmap;
+ Drawable drawable = getCoverDrawable();
+ if (drawable instanceof FitWidthBitmapDrawable) {
+ ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);
+ }
+ }
+
+ /**
+ * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.
+ *
+ * @return Bitmap for cover drawable.
+ */
+ public final Bitmap getCoverBitmap() {
+ return mCoverBitmap;
+ }
+
+ /**
+ * Returns color set by {@link #setSolidColor(int)}.
+ *
+ * @return Solid color used for bottom drawable.
+ */
+ public final @ColorInt int getSolidColor() {
+ return mSolidColor;
+ }
+
+ /**
+ * Convenient method to set color in bottom drawable. If app is not using default
+ * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()
+ * before calling {@link #enableParallax()}.
+ *
+ * @param color color for bottom drawable.
+ */
+ public final void setSolidColor(@ColorInt int color) {
+ mSolidColor = color;
+ Drawable bottomDrawable = getBottomDrawable();
+ if (bottomDrawable instanceof ColorDrawable) {
+ ((ColorDrawable) bottomDrawable).setColor(color);
+ }
+ }
+
+ /**
+ * Sets default parallax offset in pixels for bitmap moving vertically. This method must
+ * be called before {@link #enableParallax()}.
+ *
+ * @param offset Offset in pixels (e.g. 120).
+ * @see #enableParallax()
+ */
+ public final void setParallaxDrawableMaxOffset(int offset) {
+ if (mParallaxDrawable != null) {
+ throw new IllegalStateException("enableParallax already called");
+ }
+ mParallaxDrawableMaxOffset = offset;
+ }
+
+ /**
+ * Returns Default parallax offset in pixels for bitmap moving vertically.
+ * When 0, a default value would be used.
+ *
+ * @return Default parallax offset in pixels for bitmap moving vertically.
+ * @see #enableParallax()
+ */
+ public final int getParallaxDrawableMaxOffset() {
+ return mParallaxDrawableMaxOffset;
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
rename to leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
diff --git a/leanback/src/android/support/v17/leanback/app/ErrorFragment.java b/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
new file mode 100644
index 0000000..eda0de1
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
@@ -0,0 +1,247 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from ErrorSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * A fragment for displaying an error indication.
+ * @deprecated use {@link ErrorSupportFragment}
+ */
+@Deprecated
+public class ErrorFragment extends BrandedFragment {
+
+ private ViewGroup mErrorFrame;
+ private ImageView mImageView;
+ private TextView mTextView;
+ private Button mButton;
+ private Drawable mDrawable;
+ private CharSequence mMessage;
+ private String mButtonText;
+ private View.OnClickListener mButtonClickListener;
+ private Drawable mBackgroundDrawable;
+ private boolean mIsBackgroundTranslucent = true;
+
+ /**
+ * Sets the default background.
+ *
+ * @param translucent True to set a translucent background.
+ */
+ public void setDefaultBackground(boolean translucent) {
+ mBackgroundDrawable = null;
+ mIsBackgroundTranslucent = translucent;
+ updateBackground();
+ updateMessage();
+ }
+
+ /**
+ * Returns true if the background is translucent.
+ */
+ public boolean isBackgroundTranslucent() {
+ return mIsBackgroundTranslucent;
+ }
+
+ /**
+ * Sets a drawable for the fragment background.
+ *
+ * @param drawable The drawable used for the background.
+ */
+ public void setBackgroundDrawable(Drawable drawable) {
+ mBackgroundDrawable = drawable;
+ if (drawable != null) {
+ final int opacity = drawable.getOpacity();
+ mIsBackgroundTranslucent = (opacity == PixelFormat.TRANSLUCENT
+ || opacity == PixelFormat.TRANSPARENT);
+ }
+ updateBackground();
+ updateMessage();
+ }
+
+ /**
+ * Returns the background drawable. May be null if a default is used.
+ */
+ public Drawable getBackgroundDrawable() {
+ return mBackgroundDrawable;
+ }
+
+ /**
+ * Sets the drawable to be used for the error image.
+ *
+ * @param drawable The drawable used for the error image.
+ */
+ public void setImageDrawable(Drawable drawable) {
+ mDrawable = drawable;
+ updateImageDrawable();
+ }
+
+ /**
+ * Returns the drawable used for the error image.
+ */
+ public Drawable getImageDrawable() {
+ return mDrawable;
+ }
+
+ /**
+ * Sets the error message.
+ *
+ * @param message The error message.
+ */
+ public void setMessage(CharSequence message) {
+ mMessage = message;
+ updateMessage();
+ }
+
+ /**
+ * Returns the error message.
+ */
+ public CharSequence getMessage() {
+ return mMessage;
+ }
+
+ /**
+ * Sets the button text.
+ *
+ * @param text The button text.
+ */
+ public void setButtonText(String text) {
+ mButtonText = text;
+ updateButton();
+ }
+
+ /**
+ * Returns the button text.
+ */
+ public String getButtonText() {
+ return mButtonText;
+ }
+
+ /**
+ * Set the button click listener.
+ *
+ * @param clickListener The click listener for the button.
+ */
+ public void setButtonClickListener(View.OnClickListener clickListener) {
+ mButtonClickListener = clickListener;
+ updateButton();
+ }
+
+ /**
+ * Returns the button click listener.
+ */
+ public View.OnClickListener getButtonClickListener() {
+ return mButtonClickListener;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.lb_error_fragment, container, false);
+
+ mErrorFrame = (ViewGroup) root.findViewById(R.id.error_frame);
+ updateBackground();
+
+ installTitleView(inflater, mErrorFrame, savedInstanceState);
+
+ mImageView = (ImageView) root.findViewById(R.id.image);
+ updateImageDrawable();
+
+ mTextView = (TextView) root.findViewById(R.id.message);
+ updateMessage();
+
+ mButton = (Button) root.findViewById(R.id.button);
+ updateButton();
+
+ FontMetricsInt metrics = getFontMetricsInt(mTextView);
+ int underImageBaselineMargin = container.getResources().getDimensionPixelSize(
+ R.dimen.lb_error_under_image_baseline_margin);
+ setTopMargin(mTextView, underImageBaselineMargin + metrics.ascent);
+
+ int underMessageBaselineMargin = container.getResources().getDimensionPixelSize(
+ R.dimen.lb_error_under_message_baseline_margin);
+ setTopMargin(mButton, underMessageBaselineMargin - metrics.descent);
+
+ return root;
+ }
+
+ private void updateBackground() {
+ if (mErrorFrame != null) {
+ if (mBackgroundDrawable != null) {
+ mErrorFrame.setBackground(mBackgroundDrawable);
+ } else {
+ mErrorFrame.setBackgroundColor(mErrorFrame.getResources().getColor(
+ mIsBackgroundTranslucent
+ ? R.color.lb_error_background_color_translucent
+ : R.color.lb_error_background_color_opaque));
+ }
+ }
+ }
+
+ private void updateMessage() {
+ if (mTextView != null) {
+ mTextView.setText(mMessage);
+ mTextView.setVisibility(TextUtils.isEmpty(mMessage) ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void updateImageDrawable() {
+ if (mImageView != null) {
+ mImageView.setImageDrawable(mDrawable);
+ mImageView.setVisibility(mDrawable == null ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void updateButton() {
+ if (mButton != null) {
+ mButton.setText(mButtonText);
+ mButton.setOnClickListener(mButtonClickListener);
+ mButton.setVisibility(TextUtils.isEmpty(mButtonText) ? View.GONE : View.VISIBLE);
+ mButton.requestFocus();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mErrorFrame.requestFocus();
+ }
+
+ private static FontMetricsInt getFontMetricsInt(TextView textView) {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTextSize(textView.getTextSize());
+ paint.setTypeface(textView.getTypeface());
+ return paint.getFontMetricsInt();
+ }
+
+ private static void setTopMargin(TextView textView, int topMargin) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
+ lp.topMargin = topMargin;
+ textView.setLayoutParams(lp);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java b/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java b/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
rename to leanback/src/android/support/v17/leanback/app/FragmentUtil.java
diff --git a/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
new file mode 100644
index 0000000..9be350d
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -0,0 +1,1420 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.widget.DiffCallback;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionAdapter;
+import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
+import android.support.v4.app.ActivityCompat;
+import android.app.Fragment;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
+import android.app.FragmentTransaction;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A GuidedStepFragment is used to guide the user through a decision or series of decisions.
+ * It is composed of a guidance view on the left and a view on the right containing a list of
+ * possible actions.
+ * <p>
+ * <h3>Basic Usage</h3>
+ * <p>
+ * Clients of GuidedStepFragment must create a custom subclass to attach to their Activities.
+ * This custom subclass provides the information necessary to construct the user interface and
+ * respond to user actions. At a minimum, subclasses should override:
+ * <ul>
+ * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
+ * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
+ * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
+ * </ul>
+ * <p>
+ * Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager:
+ * <ul>
+ * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}, to be called during Activity onCreate,
+ * adds GuidedStepFragment as the first Fragment in activity.</li>
+ * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
+ * GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or
+ * replacing existing GuidedStepFragment when moving forward to next step.</li>
+ * <li>{@link #finishGuidedStepFragments()} can either finish the activity or pop all
+ * GuidedStepFragment from stack.
+ * <li>If app chooses not to use the helper function, it is the app's responsibility to call
+ * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
+ * need pops to.
+ * </ul>
+ * <h3>Theming and Stylists</h3>
+ * <p>
+ * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link
+ * GuidanceStylist} is responsible for the left guidance view, while the {@link
+ * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
+ * attributes to derive values associated with the presentation, such as colors, animations, etc.
+ * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
+ * via theming; see their documentation for more information.
+ * <p>
+ * GuidedStepFragments must have access to an appropriate theme in order for the stylists to
+ * function properly. Specifically, the fragment must receive {@link
+ * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
+ * is set to that theme. Themes can be provided in one of three ways:
+ * <ul>
+ * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
+ * theme that derives from it.</li>
+ * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
+ * existing Activity theme can have an entry added for the attribute {@link
+ * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
+ * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li>
+ * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link
+ * #onProvideTheme} method. This can be useful if a subclass is used across multiple
+ * Activities.</li>
+ * </ul>
+ * <p>
+ * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
+ * the Activity's theme. (Themes whose parent theme is already set to the guided step theme do not
+ * need to set the guidedStepTheme attribute; if set, it will be ignored.)
+ * <p>
+ * If themes do not provide enough customizability, the stylists themselves may be subclassed and
+ * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link
+ * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses
+ * may override layout files; subclasses may also have more complex logic to determine styling.
+ * <p>
+ * <h3>Guided sequences</h3>
+ * <p>
+ * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments
+ * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
+ * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
+ * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
+ * custom animations are properly configured. (Custom animations are triggered automatically when
+ * the fragment stack is subsequently popped by any normal mechanism.)
+ * <p>
+ * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically,
+ * rather than in XML. This restriction may be removed in the future.</i>
+ *
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation
+ * @see GuidanceStylist
+ * @see GuidanceStylist.Guidance
+ * @see GuidedAction
+ * @see GuidedActionsStylist
+ * @deprecated use {@link GuidedStepSupportFragment}
+ */
+@Deprecated
+public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener {
+
+ private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment";
+ private static final String EXTRA_ACTION_PREFIX = "action_";
+ private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_";
+
+ private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault";
+
+ private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
+
+ private static final boolean IS_FRAMEWORK_FRAGMENT = true;
+
+ /**
+ * Fragment argument name for UI style. The argument value is persisted in fragment state and
+ * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and
+ * might be changed in one of the three helper functions:
+ * <ul>
+ * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)} sets to
+ * {@link #UI_STYLE_ACTIVITY_ROOT}</li>
+ * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
+ * GuidedStepFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a
+ * GuidedStepFragment on stack.</li>
+ * <li>{@link #finishGuidedStepFragments()} changes current GuidedStepFragment to
+ * {@link #UI_STYLE_ENTRANCE} for the non activity case. This is a special case that changes
+ * the transition settings after fragment has been created, in order to force current
+ * GuidedStepFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li>
+ * </ul>
+ * <p>
+ * Argument value can be either:
+ * <ul>
+ * <li>{@link #UI_STYLE_REPLACE}</li>
+ * <li>{@link #UI_STYLE_ENTRANCE}</li>
+ * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
+ * </ul>
+ */
+ public static final String EXTRA_UI_STYLE = "uiStyle";
+
+ /**
+ * This is the case that we use GuidedStepFragment to replace another existing
+ * GuidedStepFragment when moving forward to next step. Default behavior of this style is:
+ * <ul>
+ * <li>Enter transition slides in from END(right), exit transition same as
+ * {@link #UI_STYLE_ENTRANCE}.
+ * </li>
+ * </ul>
+ */
+ public static final int UI_STYLE_REPLACE = 0;
+
+ /**
+ * @deprecated Same value as {@link #UI_STYLE_REPLACE}.
+ */
+ @Deprecated
+ public static final int UI_STYLE_DEFAULT = 0;
+
+ /**
+ * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in
+ * GuidedStepFragment constructor. This is the case that we show GuidedStepFragment on top of
+ * other content. The default behavior of this style:
+ * <ul>
+ * <li>Enter transition slides in from two sides, exit transition slide out to START(left).
+ * Background will be faded in. Note: Changing exit transition by UI style is not working
+ * because fragment transition asks for exit transition before UI style is restored in Fragment
+ * .onCreate().</li>
+ * </ul>
+ * When popping multiple GuidedStepFragment, {@link #finishGuidedStepFragments()} also changes
+ * the top GuidedStepFragment to UI_STYLE_ENTRANCE in order to run the return transition
+ * (reverse of enter transition) of UI_STYLE_ENTRANCE.
+ */
+ public static final int UI_STYLE_ENTRANCE = 1;
+
+ /**
+ * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
+ * GuidedStepFragment in a separate activity. The default behavior of this style:
+ * <ul>
+ * <li>Enter transition is assigned null (will rely on activity transition), exit transition is
+ * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working
+ * because fragment transition asks for exit transition before UI style is restored in
+ * Fragment.onCreate().</li>
+ * </ul>
+ */
+ public static final int UI_STYLE_ACTIVITY_ROOT = 2;
+
+ /**
+ * Animation to slide the contents from the side (left/right).
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int SLIDE_FROM_SIDE = 0;
+
+ /**
+ * Animation to slide the contents from the bottom.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int SLIDE_FROM_BOTTOM = 1;
+
+ private static final String TAG = "GuidedStepF";
+ private static final boolean DEBUG = false;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static class DummyFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View v = new View(inflater.getContext());
+ v.setVisibility(View.GONE);
+ return v;
+ }
+ }
+
+ private ContextThemeWrapper mThemeWrapper;
+ private GuidanceStylist mGuidanceStylist;
+ GuidedActionsStylist mActionsStylist;
+ private GuidedActionsStylist mButtonActionsStylist;
+ private GuidedActionAdapter mAdapter;
+ private GuidedActionAdapter mSubAdapter;
+ private GuidedActionAdapter mButtonAdapter;
+ private GuidedActionAdapterGroup mAdapterGroup;
+ private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
+ private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
+ private int entranceTransitionType = SLIDE_FROM_SIDE;
+
+ public GuidedStepFragment() {
+ mGuidanceStylist = onCreateGuidanceStylist();
+ mActionsStylist = onCreateActionsStylist();
+ mButtonActionsStylist = onCreateButtonActionsStylist();
+ onProvideFragmentTransitions();
+ }
+
+ /**
+ * Creates the presenter used to style the guidance panel. The default implementation returns
+ * a basic GuidanceStylist.
+ * @return The GuidanceStylist used in this fragment.
+ */
+ public GuidanceStylist onCreateGuidanceStylist() {
+ return new GuidanceStylist();
+ }
+
+ /**
+ * Creates the presenter used to style the guided actions panel. The default implementation
+ * returns a basic GuidedActionsStylist.
+ * @return The GuidedActionsStylist used in this fragment.
+ */
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist();
+ }
+
+ /**
+ * Creates the presenter used to style a sided actions panel for button only.
+ * The default implementation returns a basic GuidedActionsStylist.
+ * @return The GuidedActionsStylist used in this fragment.
+ */
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ GuidedActionsStylist stylist = new GuidedActionsStylist();
+ stylist.setAsButtonActions();
+ return stylist;
+ }
+
+ /**
+ * Returns the theme used for styling the fragment. The default returns -1, indicating that the
+ * host Activity's theme should be used.
+ * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
+ * host Activity's theme.
+ */
+ public int onProvideTheme() {
+ return -1;
+ }
+
+ /**
+ * Returns the information required to provide guidance to the user. This hook is called during
+ * {@link #onCreateView}. May be overridden to return a custom subclass of {@link
+ * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
+ * returns a Guidance object with empty fields; subclasses should override.
+ * @param savedInstanceState The saved instance state from onCreateView.
+ * @return The Guidance object representing the information used to guide the user.
+ */
+ public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance("", "", "", null);
+ }
+
+ /**
+ * Fills out the set of actions available to the user. This hook is called during {@link
+ * #onCreate}. The default leaves the list of actions empty; subclasses should override.
+ * @param actions A non-null, empty list ready to be populated.
+ * @param savedInstanceState The saved instance state from onCreate.
+ */
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ /**
+ * Fills out the set of actions shown at right available to the user. This hook is called during
+ * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
+ * @param actions A non-null, empty list ready to be populated.
+ * @param savedInstanceState The saved instance state from onCreate.
+ */
+ public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ }
+
+ /**
+ * Callback invoked when an action is taken by the user. Subclasses should override in
+ * order to act on the user's decisions.
+ * @param action The chosen action.
+ */
+ public void onGuidedActionClicked(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action in sub actions is taken by the user. Subclasses should
+ * override in order to act on the user's decisions. Default return value is true to close
+ * the sub actions list.
+ * @param action The chosen action.
+ * @return true to collapse the sub actions list, false to keep it expanded.
+ */
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return true;
+ }
+
+ /**
+ * @return True if is current expanded including subactions list or
+ * action with {@link GuidedAction#hasEditableActivatorView()} is true.
+ */
+ public boolean isExpanded() {
+ return mActionsStylist.isExpanded();
+ }
+
+ /**
+ * @return True if the sub actions list is expanded, false otherwise.
+ */
+ public boolean isSubActionsExpanded() {
+ return mActionsStylist.isSubActionsExpanded();
+ }
+
+ /**
+ * Expand a given action's sub actions list.
+ * @param action GuidedAction to expand.
+ * @see #expandAction(GuidedAction, boolean)
+ */
+ public void expandSubActions(GuidedAction action) {
+ if (!action.hasSubActions()) {
+ return;
+ }
+ expandAction(action, true);
+ }
+
+ /**
+ * Expand a given action with sub actions list or
+ * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view.
+ *
+ * @param action GuidedAction to expand.
+ * @param withTransition True to run transition animation, false otherwise.
+ */
+ public void expandAction(GuidedAction action, boolean withTransition) {
+ mActionsStylist.expandAction(action, withTransition);
+ }
+
+ /**
+ * Collapse sub actions list.
+ * @see GuidedAction#getSubActions()
+ */
+ public void collapseSubActions() {
+ collapseAction(true);
+ }
+
+ /**
+ * Collapse action which either has a sub actions list or action with
+ * {@link GuidedAction#hasEditableActivatorView()} is true.
+ *
+ * @param withTransition True to run transition animation, false otherwise.
+ */
+ public void collapseAction(boolean withTransition) {
+ if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) {
+ mActionsStylist.collapseAction(withTransition);
+ }
+ }
+
+ /**
+ * Callback invoked when an action is focused (made to be the current selection) by the user.
+ */
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action's title or description has been edited, this happens either
+ * when user clicks confirm button in IME or user closes IME window by BACK key.
+ * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or
+ * {@link #onGuidedActionEditCanceled(GuidedAction)}.
+ */
+ @Deprecated
+ public void onGuidedActionEdited(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action has been canceled editing, for example when user closes
+ * IME window by BACK key. Default implementation calls deprecated method
+ * {@link #onGuidedActionEdited(GuidedAction)}.
+ * @param action The action which has been canceled editing.
+ */
+ public void onGuidedActionEditCanceled(GuidedAction action) {
+ onGuidedActionEdited(action);
+ }
+
+ /**
+ * Callback invoked when an action has been edited, for example when user clicks confirm button
+ * in IME window. Default implementation calls deprecated method
+ * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}.
+ *
+ * @param action The action that has been edited.
+ * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
+ * {@link GuidedAction#ACTION_ID_CURRENT}.
+ */
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ onGuidedActionEdited(action);
+ return GuidedAction.ACTION_ID_NEXT;
+ }
+
+ /**
+ * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
+ * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
+ * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
+ * is pressed.
+ * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE}
+ * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <p>
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param fragmentManager The FragmentManager to be used in the transaction.
+ * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
+ * @return The ID returned by the call FragmentTransaction.commit.
+ */
+ public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) {
+ return add(fragmentManager, fragment, android.R.id.content);
+ }
+
+ /**
+ * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
+ * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
+ * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
+ * is pressed.
+ * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} and
+ * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepFragment)} will be called
+ * to perform shared element transition between GuidedStepFragments.
+ * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <p>
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param fragmentManager The FragmentManager to be used in the transaction.
+ * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
+ * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
+ * @return The ID returned by the call FragmentTransaction.commit.
+ */
+ public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) {
+ GuidedStepFragment current = getCurrentGuidedStepFragment(fragmentManager);
+ boolean inGuidedStep = current != null;
+ if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
+ && !inGuidedStep) {
+ // workaround b/22631964 for framework fragment
+ fragmentManager.beginTransaction()
+ .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
+ .commit();
+ }
+ FragmentTransaction ft = fragmentManager.beginTransaction();
+
+ fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
+ ft.addToBackStack(fragment.generateStackEntryName());
+ if (current != null) {
+ fragment.onAddSharedElementTransition(ft, current);
+ }
+ return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
+ }
+
+ /**
+ * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
+ * when the GuidedStepFragment replacing an existing GuidedStepFragment). Default implementation
+ * establishes connections between action background views to morph action background bounds
+ * change from disappearing GuidedStepFragment into this GuidedStepFragment. The default
+ * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
+ * method when modifying the default layout of {@link GuidedActionsStylist}.
+ *
+ * @see GuidedActionsStylist
+ * @see #onProvideFragmentTransitions()
+ * @param ft The FragmentTransaction to add shared element.
+ * @param disappearing The disappearing fragment.
+ */
+ protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepFragment
+ disappearing) {
+ View fragmentView = disappearing.getView();
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment_root), "action_fragment_root");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment_background), "action_fragment_background");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment), "action_fragment");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_root), "guidedactions_root");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_content), "guidedactions_content");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_list_background), "guidedactions_list_background");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_root2), "guidedactions_root2");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_content2), "guidedactions_content2");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_list_background2), "guidedactions_list_background2");
+ }
+
+ private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
+ String transitionName)
+ {
+ if (subView != null)
+ TransitionHelper.addSharedElement(ft, subView, transitionName);
+ }
+
+ /**
+ * Returns BackStackEntry name for the GuidedStepFragment or empty String if no entry is
+ * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method
+ * returns undefined value if the fragment is not in FragmentManager.
+ * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
+ * associated.
+ */
+ final String generateStackEntryName() {
+ return generateStackEntryName(getUiStyle(), getClass());
+ }
+
+ /**
+ * Generates BackStackEntry name for GuidedStepFragment class or empty String if no entry is
+ * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
+ * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
+ * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
+ * associated.
+ */
+ static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
+ switch (uiStyle) {
+ case UI_STYLE_REPLACE:
+ return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
+ case UI_STYLE_ENTRANCE:
+ return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
+ case UI_STYLE_ACTIVITY_ROOT:
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Returns true if the backstack entry represents GuidedStepFragment with
+ * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepFragment pushed to stack; false
+ * otherwise.
+ * @see #generateStackEntryName(int, Class)
+ * @param backStackEntryName Name of BackStackEntry.
+ * @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
+ * false otherwise.
+ */
+ static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
+ return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
+ }
+
+ /**
+ * Extract Class name from BackStackEntry name.
+ * @param backStackEntryName Name of BackStackEntry.
+ * @return Class name of GuidedStepFragment.
+ */
+ static String getGuidedStepFragmentClassName(String backStackEntryName) {
+ if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
+ return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
+ } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
+ return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Adds the specified GuidedStepFragment as content of Activity; no backstack entry is added so
+ * the activity will be dismissed when BACK key is pressed. The method is typically called in
+ * Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null,
+ * the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored
+ * by FragmentManager.
+ * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
+ *
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param activity The Activity to be used to insert GuidedstepFragment.
+ * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
+ * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
+ * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
+ * GuidedStepFragment.
+ */
+ public static int addAsRoot(Activity activity, GuidedStepFragment fragment, int id) {
+ // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
+ activity.getWindow().getDecorView();
+ FragmentManager fragmentManager = activity.getFragmentManager();
+ if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
+ Log.w(TAG, "Fragment is already exists, likely calling "
+ + "addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
+ return -1;
+ }
+ FragmentTransaction ft = fragmentManager.beginTransaction();
+ fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
+ return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
+ }
+
+ /**
+ * Returns the current GuidedStepFragment on the fragment transaction stack.
+ * @return The current GuidedStepFragment, if any, on the fragment transaction stack.
+ */
+ public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) {
+ Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
+ if (f instanceof GuidedStepFragment) {
+ return (GuidedStepFragment) f;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the GuidanceStylist that displays guidance information for the user.
+ * @return The GuidanceStylist for this fragment.
+ */
+ public GuidanceStylist getGuidanceStylist() {
+ return mGuidanceStylist;
+ }
+
+ /**
+ * Returns the GuidedActionsStylist that displays the actions the user may take.
+ * @return The GuidedActionsStylist for this fragment.
+ */
+ public GuidedActionsStylist getGuidedActionsStylist() {
+ return mActionsStylist;
+ }
+
+ /**
+ * Returns the list of button GuidedActions that the user may take in this fragment.
+ * @return The list of button GuidedActions for this fragment.
+ */
+ public List<GuidedAction> getButtonActions() {
+ return mButtonActions;
+ }
+
+ /**
+ * Find button GuidedAction by Id.
+ * @param id Id of the button action to search.
+ * @return GuidedAction object or null if not found.
+ */
+ public GuidedAction findButtonActionById(long id) {
+ int index = findButtonActionPositionById(id);
+ return index >= 0 ? mButtonActions.get(index) : null;
+ }
+
+ /**
+ * Find button GuidedAction position in array by Id.
+ * @param id Id of the button action to search.
+ * @return position of GuidedAction object in array or -1 if not found.
+ */
+ public int findButtonActionPositionById(long id) {
+ if (mButtonActions != null) {
+ for (int i = 0; i < mButtonActions.size(); i++) {
+ GuidedAction action = mButtonActions.get(i);
+ if (mButtonActions.get(i).getId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the GuidedActionsStylist that displays the button actions the user may take.
+ * @return The GuidedActionsStylist for this fragment.
+ */
+ public GuidedActionsStylist getGuidedButtonActionsStylist() {
+ return mButtonActionsStylist;
+ }
+
+ /**
+ * Sets the list of button GuidedActions that the user may take in this fragment.
+ * @param actions The list of button GuidedActions for this fragment.
+ */
+ public void setButtonActions(List<GuidedAction> actions) {
+ mButtonActions = actions;
+ if (mButtonAdapter != null) {
+ mButtonAdapter.setActions(mButtonActions);
+ }
+ }
+
+ /**
+ * Notify an button action has changed and update its UI.
+ * @param position Position of the button GuidedAction in array.
+ */
+ public void notifyButtonActionChanged(int position) {
+ if (mButtonAdapter != null) {
+ mButtonAdapter.notifyItemChanged(position);
+ }
+ }
+
+ /**
+ * Returns the view corresponding to the button action at the indicated position in the list of
+ * actions for this fragment.
+ * @param position The integer position of the button action of interest.
+ * @return The View corresponding to the button action at the indicated position, or null if
+ * that action is not currently onscreen.
+ */
+ public View getButtonActionItemView(int position) {
+ final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
+ .findViewHolderForPosition(position);
+ return holder == null ? null : holder.itemView;
+ }
+
+ /**
+ * Scrolls the action list to the position indicated, selecting that button action's view.
+ * @param position The integer position of the button action of interest.
+ */
+ public void setSelectedButtonActionPosition(int position) {
+ mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the position if the currently selected button GuidedAction.
+ * @return position The integer position of the currently selected button action.
+ */
+ public int getSelectedButtonActionPosition() {
+ return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
+ }
+
+ /**
+ * Returns the list of GuidedActions that the user may take in this fragment.
+ * @return The list of GuidedActions for this fragment.
+ */
+ public List<GuidedAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Find GuidedAction by Id.
+ * @param id Id of the action to search.
+ * @return GuidedAction object or null if not found.
+ */
+ public GuidedAction findActionById(long id) {
+ int index = findActionPositionById(id);
+ return index >= 0 ? mActions.get(index) : null;
+ }
+
+ /**
+ * Find GuidedAction position in array by Id.
+ * @param id Id of the action to search.
+ * @return position of GuidedAction object in array or -1 if not found.
+ */
+ public int findActionPositionById(long id) {
+ if (mActions != null) {
+ for (int i = 0; i < mActions.size(); i++) {
+ GuidedAction action = mActions.get(i);
+ if (mActions.get(i).getId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Sets the list of GuidedActions that the user may take in this fragment.
+ * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}.
+ *
+ * @param actions The list of GuidedActions for this fragment.
+ */
+ public void setActions(List<GuidedAction> actions) {
+ mActions = actions;
+ if (mAdapter != null) {
+ mAdapter.setActions(mActions);
+ }
+ }
+
+ /**
+ * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default
+ * GuidedStepFragment uses
+ * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}.
+ * Sets it to null if app wants to refresh the whole list.
+ *
+ * @param diffCallback DiffCallback used in {@link #setActions(List)}.
+ */
+ public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+ mAdapter.setDiffCallback(diffCallback);
+ }
+
+ /**
+ * Notify an action has changed and update its UI.
+ * @param position Position of the GuidedAction in array.
+ */
+ public void notifyActionChanged(int position) {
+ if (mAdapter != null) {
+ mAdapter.notifyItemChanged(position);
+ }
+ }
+
+ /**
+ * Returns the view corresponding to the action at the indicated position in the list of
+ * actions for this fragment.
+ * @param position The integer position of the action of interest.
+ * @return The View corresponding to the action at the indicated position, or null if that
+ * action is not currently onscreen.
+ */
+ public View getActionItemView(int position) {
+ final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
+ .findViewHolderForPosition(position);
+ return holder == null ? null : holder.itemView;
+ }
+
+ /**
+ * Scrolls the action list to the position indicated, selecting that action's view.
+ * @param position The integer position of the action of interest.
+ */
+ public void setSelectedActionPosition(int position) {
+ mActionsStylist.getActionsGridView().setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the position if the currently selected GuidedAction.
+ * @return position The integer position of the currently selected action.
+ */
+ public int getSelectedActionPosition() {
+ return mActionsStylist.getActionsGridView().getSelectedPosition();
+ }
+
+ /**
+ * Called by Constructor to provide fragment transitions. The default implementation assigns
+ * transitions based on {@link #getUiStyle()}:
+ * <ul>
+ * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
+ * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
+ * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
+ * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
+ * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
+ * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
+ * enter transition.
+ * </ul>
+ * <p>
+ * The default implementation heavily relies on {@link GuidedActionsStylist} and
+ * {@link GuidanceStylist} layout, app may override this method when modifying the default
+ * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
+ * <p>
+ * TIP: because the fragment view is removed during fragment transition, in general app cannot
+ * use two Visibility transition together. Workaround is to create your own Visibility
+ * transition that controls multiple animators (e.g. slide and fade animation in one Transition
+ * class).
+ */
+ protected void onProvideFragmentTransitions() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ final int uiStyle = getUiStyle();
+ if (uiStyle == UI_STYLE_REPLACE) {
+ Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
+ TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
+ TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
+ true);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+
+ Object fade = TransitionHelper.createFadeTransition(
+ TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
+ TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
+ Object changeBounds = TransitionHelper.createChangeBounds(false);
+ Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(sharedElementTransition, fade);
+ TransitionHelper.addTransition(sharedElementTransition, changeBounds);
+ TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
+ } else if (uiStyle == UI_STYLE_ENTRANCE) {
+ if (entranceTransitionType == SLIDE_FROM_SIDE) {
+ Object fade = TransitionHelper.createFadeTransition(
+ TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
+ TransitionHelper.include(fade, R.id.guidedstep_background);
+ Object slideFromSide = TransitionHelper.createFadeAndShortSlide(
+ Gravity.END | Gravity.START);
+ TransitionHelper.include(slideFromSide, R.id.content_fragment);
+ TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
+ Object enterTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(enterTransition, fade);
+ TransitionHelper.addTransition(enterTransition, slideFromSide);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+ } else {
+ Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
+ Gravity.BOTTOM);
+ TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
+ Object enterTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(enterTransition, slideFromBottom);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+ }
+ // No shared element transition
+ TransitionHelper.setSharedElementEnterTransition(this, null);
+ } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
+ // for Activity root, we don't need enter transition, use activity transition
+ TransitionHelper.setEnterTransition(this, null);
+ // No shared element transition
+ TransitionHelper.setSharedElementEnterTransition(this, null);
+ }
+ // exitTransition is same for all style
+ Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
+ TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
+ TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
+ true);
+ TransitionHelper.setExitTransition(this, exitTransition);
+ }
+ }
+
+ /**
+ * Called by onCreateView to inflate background view. Default implementation loads view
+ * from {@link R.layout#lb_guidedstep_background} which holds a reference to
+ * guidedStepBackground.
+ * @param inflater LayoutInflater to load background view.
+ * @param container Parent view of background view.
+ * @param savedInstanceState
+ * @return Created background view or null if no background.
+ */
+ public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
+ }
+
+ /**
+ * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
+ * is first initialized. UI style is used to choose different fragment transition animations and
+ * determine if this is the first GuidedStepFragment on backstack. In most cases app does not
+ * directly call this method, app calls helper function
+ * {@link #add(FragmentManager, GuidedStepFragment, int)}. However if the app creates Fragment
+ * transaction and controls backstack by itself, it would need call setUiStyle() to select the
+ * fragment transition to use.
+ *
+ * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
+ * {@link #UI_STYLE_ENTRANCE}.
+ */
+ public void setUiStyle(int style) {
+ int oldStyle = getUiStyle();
+ Bundle arguments = getArguments();
+ boolean isNew = false;
+ if (arguments == null) {
+ arguments = new Bundle();
+ isNew = true;
+ }
+ arguments.putInt(EXTRA_UI_STYLE, style);
+ // call setArgument() will validate if the fragment is already added.
+ if (isNew) {
+ setArguments(arguments);
+ }
+ if (style != oldStyle) {
+ onProvideFragmentTransitions();
+ }
+ }
+
+ /**
+ * Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when
+ * fragment is first initialized. UI style is used to choose different fragment transition
+ * animations and determine if this is the first GuidedStepFragment on backstack.
+ *
+ * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
+ * {@link #UI_STYLE_ENTRANCE}.
+ * @see #onProvideFragmentTransitions()
+ */
+ public int getUiStyle() {
+ Bundle b = getArguments();
+ if (b == null) return UI_STYLE_ENTRANCE;
+ return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (DEBUG) Log.v(TAG, "onCreate");
+ // Set correct transition from saved arguments.
+ onProvideFragmentTransitions();
+
+ ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
+ onCreateActions(actions, savedInstanceState);
+ if (savedInstanceState != null) {
+ onRestoreActions(actions, savedInstanceState);
+ }
+ setActions(actions);
+ ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
+ onCreateButtonActions(buttonActions, savedInstanceState);
+ if (savedInstanceState != null) {
+ onRestoreButtonActions(buttonActions, savedInstanceState);
+ }
+ setButtonActions(buttonActions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroyView() {
+ mGuidanceStylist.onDestroyView();
+ mActionsStylist.onDestroyView();
+ mButtonActionsStylist.onDestroyView();
+ mAdapter = null;
+ mSubAdapter = null;
+ mButtonAdapter = null;
+ mAdapterGroup = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (DEBUG) Log.v(TAG, "onCreateView");
+
+ resolveTheme();
+ inflater = getThemeInflater(inflater);
+
+ GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate(
+ R.layout.lb_guidedstep_fragment, container, false);
+
+ root.setFocusOutStart(isFocusOutStartAllowed());
+ root.setFocusOutEnd(isFocusOutEndAllowed());
+
+ ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
+ ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
+ ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
+
+ Guidance guidance = onCreateGuidance(savedInstanceState);
+ View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
+ guidanceContainer.addView(guidanceView);
+
+ View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
+ actionContainer.addView(actionsView);
+
+ View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
+ actionContainer.addView(buttonActionsView);
+
+ GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
+
+ @Override
+ public void onImeOpen() {
+ runImeAnimations(true);
+ }
+
+ @Override
+ public void onImeClose() {
+ runImeAnimations(false);
+ }
+
+ @Override
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ return GuidedStepFragment.this.onGuidedActionEditedAndProceed(action);
+ }
+
+ @Override
+ public void onGuidedActionEditCanceled(GuidedAction action) {
+ GuidedStepFragment.this.onGuidedActionEditCanceled(action);
+ }
+ };
+
+ mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ GuidedStepFragment.this.onGuidedActionClicked(action);
+ if (isExpanded()) {
+ collapseAction(true);
+ } else if (action.hasSubActions() || action.hasEditableActivatorView()) {
+ expandAction(action, true);
+ }
+ }
+ }, this, mActionsStylist, false);
+ mButtonAdapter =
+ new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ GuidedStepFragment.this.onGuidedActionClicked(action);
+ }
+ }, this, mButtonActionsStylist, false);
+ mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (mActionsStylist.isInExpandTransition()) {
+ return;
+ }
+ if (GuidedStepFragment.this.onSubGuidedActionClicked(action)) {
+ collapseSubActions();
+ }
+ }
+ }, this, mActionsStylist, true);
+ mAdapterGroup = new GuidedActionAdapterGroup();
+ mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
+ mAdapterGroup.addAdpter(mSubAdapter, null);
+ mAdapterGroup.setEditListener(editListener);
+ mActionsStylist.setEditListener(editListener);
+
+ mActionsStylist.getActionsGridView().setAdapter(mAdapter);
+ if (mActionsStylist.getSubActionsGridView() != null) {
+ mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter);
+ }
+ mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
+ if (mButtonActions.size() == 0) {
+ // when there is no button actions, we don't need show the second panel, but keep
+ // the width zero to run ChangeBounds transition.
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+ buttonActionsView.getLayoutParams();
+ lp.weight = 0;
+ buttonActionsView.setLayoutParams(lp);
+ } else {
+ // when there are two actions panel, we need adjust the weight of action to
+ // guidedActionContentWidthWeightTwoPanels.
+ Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(GuidedStepFragment.this);
+ TypedValue typedValue = new TypedValue();
+ if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
+ typedValue, true)) {
+ View actionsRoot = root.findViewById(R.id.action_fragment_root);
+ float weight = typedValue.getFloat();
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
+ .getLayoutParams();
+ lp.weight = weight;
+ actionsRoot.setLayoutParams(lp);
+ }
+ }
+
+ // Add the background view.
+ View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState);
+ if (backgroundView != null) {
+ FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById(
+ R.id.guidedstep_background_view_root);
+ backgroundViewRoot.addView(backgroundView, 0);
+ }
+
+ return root;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getView().findViewById(R.id.action_fragment).requestFocus();
+ }
+
+ /**
+ * Get the key will be used to save GuidedAction with Fragment.
+ * @param action GuidedAction to get key.
+ * @return Key to save the GuidedAction.
+ */
+ final String getAutoRestoreKey(GuidedAction action) {
+ return EXTRA_ACTION_PREFIX + action.getId();
+ }
+
+ /**
+ * Get the key will be used to save GuidedAction with Fragment.
+ * @param action GuidedAction to get key.
+ * @return Key to save the GuidedAction.
+ */
+ final String getButtonAutoRestoreKey(GuidedAction action) {
+ return EXTRA_BUTTON_ACTION_PREFIX + action.getId();
+ }
+
+ final static boolean isSaveEnabled(GuidedAction action) {
+ return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID;
+ }
+
+ final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onSaveActions(List<GuidedAction> actions, Bundle outState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onSaveInstanceState(outState, getAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action));
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ onSaveActions(mActions, outState);
+ onSaveButtonActions(mButtonActions, outState);
+ }
+
+ private static boolean isGuidedStepTheme(Context context) {
+ int resId = R.attr.guidedStepThemeFlag;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
+ return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
+ }
+
+ /**
+ * Convenient method to close GuidedStepFragments on top of other content or finish Activity if
+ * GuidedStepFragments were started in a separate activity. Pops all stack entries including
+ * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
+ * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepFragment,
+ * int)} which sets up the stack entry name for finding which fragment we need to pop back to.
+ */
+ public void finishGuidedStepFragments() {
+ final FragmentManager fragmentManager = getFragmentManager();
+ final int entryCount = fragmentManager.getBackStackEntryCount();
+ if (entryCount > 0) {
+ for (int i = entryCount - 1; i >= 0; i--) {
+ BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
+ if (isStackEntryUiStyleEntrance(entry.getName())) {
+ GuidedStepFragment top = getCurrentGuidedStepFragment(fragmentManager);
+ if (top != null) {
+ top.setUiStyle(UI_STYLE_ENTRANCE);
+ }
+ fragmentManager.popBackStackImmediate(entry.getId(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ return;
+ }
+ }
+ }
+ ActivityCompat.finishAfterTransition(getActivity());
+ }
+
+ /**
+ * Convenient method to pop to fragment with Given class.
+ * @param guidedStepFragmentClass Name of the Class of GuidedStepFragment to pop to.
+ * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
+ */
+ public void popBackStackToGuidedStepFragment(Class guidedStepFragmentClass, int flags) {
+ if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
+ return;
+ }
+ final FragmentManager fragmentManager = getFragmentManager();
+ final int entryCount = fragmentManager.getBackStackEntryCount();
+ String className = guidedStepFragmentClass.getName();
+ if (entryCount > 0) {
+ for (int i = entryCount - 1; i >= 0; i--) {
+ BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
+ String entryClassName = getGuidedStepFragmentClassName(entry.getName());
+ if (className.equals(entryClassName)) {
+ fragmentManager.popBackStackImmediate(entry.getId(), flags);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if allows focus out of start edge of GuidedStepFragment, false otherwise.
+ * Default value is false, the reason is to disable FocusFinder to find focusable views
+ * beneath content of GuidedStepFragment. Subclass may override.
+ * @return True if allows focus out of start edge of GuidedStepFragment.
+ */
+ public boolean isFocusOutStartAllowed() {
+ return false;
+ }
+
+ /**
+ * Returns true if allows focus out of end edge of GuidedStepFragment, false otherwise.
+ * Default value is false, the reason is to disable FocusFinder to find focusable views
+ * beneath content of GuidedStepFragment. Subclass may override.
+ * @return True if allows focus out of end edge of GuidedStepFragment.
+ */
+ public boolean isFocusOutEndAllowed() {
+ return false;
+ }
+
+ /**
+ * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation.
+ * Currently we provide 2 different variations for animation - slide in from
+ * side (default) or bottom.
+ *
+ * Ideally we can retrieve the screen mode settings from the theme attribute
+ * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to
+ * determine the transition. But the fragment context to retrieve the theme
+ * isn't available on platform v23 or earlier.
+ *
+ * For now clients(subclasses) can call this method inside the constructor.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setEntranceTransitionType(int transitionType) {
+ this.entranceTransitionType = transitionType;
+ }
+
+ /**
+ * Opens the provided action in edit mode and raises ime. This can be
+ * used to programmatically skip the extra click required to go into edit mode. This method
+ * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ */
+ public void openInEditMode(GuidedAction action) {
+ mActionsStylist.openInEditMode(action);
+ }
+
+ private void resolveTheme() {
+ // Look up the guidedStepTheme in the currently specified theme. If it exists,
+ // replace the theme with its value.
+ Context context = FragmentUtil.getContext(GuidedStepFragment.this);
+ int theme = onProvideTheme();
+ if (theme == -1 && !isGuidedStepTheme(context)) {
+ // Look up the guidedStepTheme in the activity's currently specified theme. If it
+ // exists, replace the theme with its value.
+ int resId = R.attr.guidedStepTheme;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
+ if (found) {
+ ContextThemeWrapper themeWrapper =
+ new ContextThemeWrapper(context, typedValue.resourceId);
+ if (isGuidedStepTheme(themeWrapper)) {
+ mThemeWrapper = themeWrapper;
+ } else {
+ found = false;
+ mThemeWrapper = null;
+ }
+ }
+ if (!found) {
+ Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
+ }
+ } else if (theme != -1) {
+ mThemeWrapper = new ContextThemeWrapper(context, theme);
+ }
+ }
+
+ private LayoutInflater getThemeInflater(LayoutInflater inflater) {
+ if (mThemeWrapper == null) {
+ return inflater;
+ } else {
+ return inflater.cloneInContext(mThemeWrapper);
+ }
+ }
+
+ private int getFirstCheckedAction() {
+ for (int i = 0, size = mActions.size(); i < size; i++) {
+ if (mActions.get(i).isChecked()) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ void runImeAnimations(boolean entering) {
+ ArrayList<Animator> animators = new ArrayList<Animator>();
+ if (entering) {
+ mGuidanceStylist.onImeAppearing(animators);
+ mActionsStylist.onImeAppearing(animators);
+ mButtonActionsStylist.onImeAppearing(animators);
+ } else {
+ mGuidanceStylist.onImeDisappearing(animators);
+ mActionsStylist.onImeDisappearing(animators);
+ mButtonActionsStylist.onImeDisappearing(animators);
+ }
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animators);
+ set.start();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java b/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
rename to leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
diff --git a/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
new file mode 100644
index 0000000..e276d07
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
@@ -0,0 +1,1415 @@
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.widget.DiffCallback;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionAdapter;
+import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.BackStackEntry;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A GuidedStepSupportFragment is used to guide the user through a decision or series of decisions.
+ * It is composed of a guidance view on the left and a view on the right containing a list of
+ * possible actions.
+ * <p>
+ * <h3>Basic Usage</h3>
+ * <p>
+ * Clients of GuidedStepSupportFragment must create a custom subclass to attach to their Activities.
+ * This custom subclass provides the information necessary to construct the user interface and
+ * respond to user actions. At a minimum, subclasses should override:
+ * <ul>
+ * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
+ * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
+ * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
+ * </ul>
+ * <p>
+ * Clients use following helper functions to add GuidedStepSupportFragment to Activity or FragmentManager:
+ * <ul>
+ * <li>{@link #addAsRoot(FragmentActivity, GuidedStepSupportFragment, int)}, to be called during Activity onCreate,
+ * adds GuidedStepSupportFragment as the first Fragment in activity.</li>
+ * <li>{@link #add(FragmentManager, GuidedStepSupportFragment)} or {@link #add(FragmentManager,
+ * GuidedStepSupportFragment, int)}, to add GuidedStepSupportFragment on top of existing Fragments or
+ * replacing existing GuidedStepSupportFragment when moving forward to next step.</li>
+ * <li>{@link #finishGuidedStepSupportFragments()} can either finish the activity or pop all
+ * GuidedStepSupportFragment from stack.
+ * <li>If app chooses not to use the helper function, it is the app's responsibility to call
+ * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
+ * need pops to.
+ * </ul>
+ * <h3>Theming and Stylists</h3>
+ * <p>
+ * GuidedStepSupportFragment delegates its visual styling to classes called stylists. The {@link
+ * GuidanceStylist} is responsible for the left guidance view, while the {@link
+ * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
+ * attributes to derive values associated with the presentation, such as colors, animations, etc.
+ * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
+ * via theming; see their documentation for more information.
+ * <p>
+ * GuidedStepSupportFragments must have access to an appropriate theme in order for the stylists to
+ * function properly. Specifically, the fragment must receive {@link
+ * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
+ * is set to that theme. Themes can be provided in one of three ways:
+ * <ul>
+ * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
+ * theme that derives from it.</li>
+ * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
+ * existing Activity theme can have an entry added for the attribute {@link
+ * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
+ * this theme will be used by GuidedStepSupportFragment as an overlay to the Activity's theme.</li>
+ * <li>Finally, custom subclasses of GuidedStepSupportFragment may provide a theme through the {@link
+ * #onProvideTheme} method. This can be useful if a subclass is used across multiple
+ * Activities.</li>
+ * </ul>
+ * <p>
+ * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
+ * the Activity's theme. (Themes whose parent theme is already set to the guided step theme do not
+ * need to set the guidedStepTheme attribute; if set, it will be ignored.)
+ * <p>
+ * If themes do not provide enough customizability, the stylists themselves may be subclassed and
+ * provided to the GuidedStepSupportFragment through the {@link #onCreateGuidanceStylist} and {@link
+ * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses
+ * may override layout files; subclasses may also have more complex logic to determine styling.
+ * <p>
+ * <h3>Guided sequences</h3>
+ * <p>
+ * GuidedStepSupportFragments can be grouped together to provide a guided sequence. GuidedStepSupportFragments
+ * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
+ * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
+ * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
+ * custom animations are properly configured. (Custom animations are triggered automatically when
+ * the fragment stack is subsequently popped by any normal mechanism.)
+ * <p>
+ * <i>Note: Currently GuidedStepSupportFragments grouped in this way must all be defined programmatically,
+ * rather than in XML. This restriction may be removed in the future.</i>
+ *
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation
+ * @see GuidanceStylist
+ * @see GuidanceStylist.Guidance
+ * @see GuidedAction
+ * @see GuidedActionsStylist
+ */
+public class GuidedStepSupportFragment extends Fragment implements GuidedActionAdapter.FocusListener {
+
+ private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepSupportFragment";
+ private static final String EXTRA_ACTION_PREFIX = "action_";
+ private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_";
+
+ private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault";
+
+ private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
+
+ private static final boolean IS_FRAMEWORK_FRAGMENT = false;
+
+ /**
+ * Fragment argument name for UI style. The argument value is persisted in fragment state and
+ * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and
+ * might be changed in one of the three helper functions:
+ * <ul>
+ * <li>{@link #addAsRoot(FragmentActivity, GuidedStepSupportFragment, int)} sets to
+ * {@link #UI_STYLE_ACTIVITY_ROOT}</li>
+ * <li>{@link #add(FragmentManager, GuidedStepSupportFragment)} or {@link #add(FragmentManager,
+ * GuidedStepSupportFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a
+ * GuidedStepSupportFragment on stack.</li>
+ * <li>{@link #finishGuidedStepSupportFragments()} changes current GuidedStepSupportFragment to
+ * {@link #UI_STYLE_ENTRANCE} for the non activity case. This is a special case that changes
+ * the transition settings after fragment has been created, in order to force current
+ * GuidedStepSupportFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li>
+ * </ul>
+ * <p>
+ * Argument value can be either:
+ * <ul>
+ * <li>{@link #UI_STYLE_REPLACE}</li>
+ * <li>{@link #UI_STYLE_ENTRANCE}</li>
+ * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
+ * </ul>
+ */
+ public static final String EXTRA_UI_STYLE = "uiStyle";
+
+ /**
+ * This is the case that we use GuidedStepSupportFragment to replace another existing
+ * GuidedStepSupportFragment when moving forward to next step. Default behavior of this style is:
+ * <ul>
+ * <li>Enter transition slides in from END(right), exit transition same as
+ * {@link #UI_STYLE_ENTRANCE}.
+ * </li>
+ * </ul>
+ */
+ public static final int UI_STYLE_REPLACE = 0;
+
+ /**
+ * @deprecated Same value as {@link #UI_STYLE_REPLACE}.
+ */
+ @Deprecated
+ public static final int UI_STYLE_DEFAULT = 0;
+
+ /**
+ * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in
+ * GuidedStepSupportFragment constructor. This is the case that we show GuidedStepSupportFragment on top of
+ * other content. The default behavior of this style:
+ * <ul>
+ * <li>Enter transition slides in from two sides, exit transition slide out to START(left).
+ * Background will be faded in. Note: Changing exit transition by UI style is not working
+ * because fragment transition asks for exit transition before UI style is restored in Fragment
+ * .onCreate().</li>
+ * </ul>
+ * When popping multiple GuidedStepSupportFragment, {@link #finishGuidedStepSupportFragments()} also changes
+ * the top GuidedStepSupportFragment to UI_STYLE_ENTRANCE in order to run the return transition
+ * (reverse of enter transition) of UI_STYLE_ENTRANCE.
+ */
+ public static final int UI_STYLE_ENTRANCE = 1;
+
+ /**
+ * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
+ * GuidedStepSupportFragment in a separate activity. The default behavior of this style:
+ * <ul>
+ * <li>Enter transition is assigned null (will rely on activity transition), exit transition is
+ * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working
+ * because fragment transition asks for exit transition before UI style is restored in
+ * Fragment.onCreate().</li>
+ * </ul>
+ */
+ public static final int UI_STYLE_ACTIVITY_ROOT = 2;
+
+ /**
+ * Animation to slide the contents from the side (left/right).
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int SLIDE_FROM_SIDE = 0;
+
+ /**
+ * Animation to slide the contents from the bottom.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int SLIDE_FROM_BOTTOM = 1;
+
+ private static final String TAG = "GuidedStepF";
+ private static final boolean DEBUG = false;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static class DummyFragment extends Fragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View v = new View(inflater.getContext());
+ v.setVisibility(View.GONE);
+ return v;
+ }
+ }
+
+ private ContextThemeWrapper mThemeWrapper;
+ private GuidanceStylist mGuidanceStylist;
+ GuidedActionsStylist mActionsStylist;
+ private GuidedActionsStylist mButtonActionsStylist;
+ private GuidedActionAdapter mAdapter;
+ private GuidedActionAdapter mSubAdapter;
+ private GuidedActionAdapter mButtonAdapter;
+ private GuidedActionAdapterGroup mAdapterGroup;
+ private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
+ private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
+ private int entranceTransitionType = SLIDE_FROM_SIDE;
+
+ public GuidedStepSupportFragment() {
+ mGuidanceStylist = onCreateGuidanceStylist();
+ mActionsStylist = onCreateActionsStylist();
+ mButtonActionsStylist = onCreateButtonActionsStylist();
+ onProvideFragmentTransitions();
+ }
+
+ /**
+ * Creates the presenter used to style the guidance panel. The default implementation returns
+ * a basic GuidanceStylist.
+ * @return The GuidanceStylist used in this fragment.
+ */
+ public GuidanceStylist onCreateGuidanceStylist() {
+ return new GuidanceStylist();
+ }
+
+ /**
+ * Creates the presenter used to style the guided actions panel. The default implementation
+ * returns a basic GuidedActionsStylist.
+ * @return The GuidedActionsStylist used in this fragment.
+ */
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist();
+ }
+
+ /**
+ * Creates the presenter used to style a sided actions panel for button only.
+ * The default implementation returns a basic GuidedActionsStylist.
+ * @return The GuidedActionsStylist used in this fragment.
+ */
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ GuidedActionsStylist stylist = new GuidedActionsStylist();
+ stylist.setAsButtonActions();
+ return stylist;
+ }
+
+ /**
+ * Returns the theme used for styling the fragment. The default returns -1, indicating that the
+ * host Activity's theme should be used.
+ * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
+ * host Activity's theme.
+ */
+ public int onProvideTheme() {
+ return -1;
+ }
+
+ /**
+ * Returns the information required to provide guidance to the user. This hook is called during
+ * {@link #onCreateView}. May be overridden to return a custom subclass of {@link
+ * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
+ * returns a Guidance object with empty fields; subclasses should override.
+ * @param savedInstanceState The saved instance state from onCreateView.
+ * @return The Guidance object representing the information used to guide the user.
+ */
+ public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance("", "", "", null);
+ }
+
+ /**
+ * Fills out the set of actions available to the user. This hook is called during {@link
+ * #onCreate}. The default leaves the list of actions empty; subclasses should override.
+ * @param actions A non-null, empty list ready to be populated.
+ * @param savedInstanceState The saved instance state from onCreate.
+ */
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ /**
+ * Fills out the set of actions shown at right available to the user. This hook is called during
+ * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
+ * @param actions A non-null, empty list ready to be populated.
+ * @param savedInstanceState The saved instance state from onCreate.
+ */
+ public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ }
+
+ /**
+ * Callback invoked when an action is taken by the user. Subclasses should override in
+ * order to act on the user's decisions.
+ * @param action The chosen action.
+ */
+ public void onGuidedActionClicked(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action in sub actions is taken by the user. Subclasses should
+ * override in order to act on the user's decisions. Default return value is true to close
+ * the sub actions list.
+ * @param action The chosen action.
+ * @return true to collapse the sub actions list, false to keep it expanded.
+ */
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return true;
+ }
+
+ /**
+ * @return True if is current expanded including subactions list or
+ * action with {@link GuidedAction#hasEditableActivatorView()} is true.
+ */
+ public boolean isExpanded() {
+ return mActionsStylist.isExpanded();
+ }
+
+ /**
+ * @return True if the sub actions list is expanded, false otherwise.
+ */
+ public boolean isSubActionsExpanded() {
+ return mActionsStylist.isSubActionsExpanded();
+ }
+
+ /**
+ * Expand a given action's sub actions list.
+ * @param action GuidedAction to expand.
+ * @see #expandAction(GuidedAction, boolean)
+ */
+ public void expandSubActions(GuidedAction action) {
+ if (!action.hasSubActions()) {
+ return;
+ }
+ expandAction(action, true);
+ }
+
+ /**
+ * Expand a given action with sub actions list or
+ * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after
+ * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view.
+ *
+ * @param action GuidedAction to expand.
+ * @param withTransition True to run transition animation, false otherwise.
+ */
+ public void expandAction(GuidedAction action, boolean withTransition) {
+ mActionsStylist.expandAction(action, withTransition);
+ }
+
+ /**
+ * Collapse sub actions list.
+ * @see GuidedAction#getSubActions()
+ */
+ public void collapseSubActions() {
+ collapseAction(true);
+ }
+
+ /**
+ * Collapse action which either has a sub actions list or action with
+ * {@link GuidedAction#hasEditableActivatorView()} is true.
+ *
+ * @param withTransition True to run transition animation, false otherwise.
+ */
+ public void collapseAction(boolean withTransition) {
+ if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) {
+ mActionsStylist.collapseAction(withTransition);
+ }
+ }
+
+ /**
+ * Callback invoked when an action is focused (made to be the current selection) by the user.
+ */
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action's title or description has been edited, this happens either
+ * when user clicks confirm button in IME or user closes IME window by BACK key.
+ * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or
+ * {@link #onGuidedActionEditCanceled(GuidedAction)}.
+ */
+ @Deprecated
+ public void onGuidedActionEdited(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action has been canceled editing, for example when user closes
+ * IME window by BACK key. Default implementation calls deprecated method
+ * {@link #onGuidedActionEdited(GuidedAction)}.
+ * @param action The action which has been canceled editing.
+ */
+ public void onGuidedActionEditCanceled(GuidedAction action) {
+ onGuidedActionEdited(action);
+ }
+
+ /**
+ * Callback invoked when an action has been edited, for example when user clicks confirm button
+ * in IME window. Default implementation calls deprecated method
+ * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}.
+ *
+ * @param action The action that has been edited.
+ * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
+ * {@link GuidedAction#ACTION_ID_CURRENT}.
+ */
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ onGuidedActionEdited(action);
+ return GuidedAction.ACTION_ID_NEXT;
+ }
+
+ /**
+ * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
+ * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
+ * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
+ * is pressed.
+ * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE}
+ * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <p>
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param fragmentManager The FragmentManager to be used in the transaction.
+ * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
+ * @return The ID returned by the call FragmentTransaction.commit.
+ */
+ public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment) {
+ return add(fragmentManager, fragment, android.R.id.content);
+ }
+
+ /**
+ * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
+ * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
+ * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
+ * is pressed.
+ * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE} and
+ * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepSupportFragment)} will be called
+ * to perform shared element transition between GuidedStepSupportFragments.
+ * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <p>
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param fragmentManager The FragmentManager to be used in the transaction.
+ * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
+ * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
+ * @return The ID returned by the call FragmentTransaction.commit.
+ */
+ public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment, int id) {
+ GuidedStepSupportFragment current = getCurrentGuidedStepSupportFragment(fragmentManager);
+ boolean inGuidedStep = current != null;
+ if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
+ && !inGuidedStep) {
+ // workaround b/22631964 for framework fragment
+ fragmentManager.beginTransaction()
+ .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
+ .commit();
+ }
+ FragmentTransaction ft = fragmentManager.beginTransaction();
+
+ fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
+ ft.addToBackStack(fragment.generateStackEntryName());
+ if (current != null) {
+ fragment.onAddSharedElementTransition(ft, current);
+ }
+ return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
+ }
+
+ /**
+ * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
+ * when the GuidedStepSupportFragment replacing an existing GuidedStepSupportFragment). Default implementation
+ * establishes connections between action background views to morph action background bounds
+ * change from disappearing GuidedStepSupportFragment into this GuidedStepSupportFragment. The default
+ * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
+ * method when modifying the default layout of {@link GuidedActionsStylist}.
+ *
+ * @see GuidedActionsStylist
+ * @see #onProvideFragmentTransitions()
+ * @param ft The FragmentTransaction to add shared element.
+ * @param disappearing The disappearing fragment.
+ */
+ protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepSupportFragment
+ disappearing) {
+ View fragmentView = disappearing.getView();
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment_root), "action_fragment_root");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment_background), "action_fragment_background");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.action_fragment), "action_fragment");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_root), "guidedactions_root");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_content), "guidedactions_content");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_list_background), "guidedactions_list_background");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_root2), "guidedactions_root2");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_content2), "guidedactions_content2");
+ addNonNullSharedElementTransition(ft, fragmentView.findViewById(
+ R.id.guidedactions_list_background2), "guidedactions_list_background2");
+ }
+
+ private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
+ String transitionName)
+ {
+ if (subView != null)
+ TransitionHelper.addSharedElement(ft, subView, transitionName);
+ }
+
+ /**
+ * Returns BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
+ * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method
+ * returns undefined value if the fragment is not in FragmentManager.
+ * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
+ * associated.
+ */
+ final String generateStackEntryName() {
+ return generateStackEntryName(getUiStyle(), getClass());
+ }
+
+ /**
+ * Generates BackStackEntry name for GuidedStepSupportFragment class or empty String if no entry is
+ * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
+ * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
+ * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
+ * associated.
+ */
+ static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
+ switch (uiStyle) {
+ case UI_STYLE_REPLACE:
+ return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
+ case UI_STYLE_ENTRANCE:
+ return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
+ case UI_STYLE_ACTIVITY_ROOT:
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Returns true if the backstack entry represents GuidedStepSupportFragment with
+ * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepSupportFragment pushed to stack; false
+ * otherwise.
+ * @see #generateStackEntryName(int, Class)
+ * @param backStackEntryName Name of BackStackEntry.
+ * @return True if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_ENTRANCE};
+ * false otherwise.
+ */
+ static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
+ return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
+ }
+
+ /**
+ * Extract Class name from BackStackEntry name.
+ * @param backStackEntryName Name of BackStackEntry.
+ * @return Class name of GuidedStepSupportFragment.
+ */
+ static String getGuidedStepSupportFragmentClassName(String backStackEntryName) {
+ if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
+ return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
+ } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
+ return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Adds the specified GuidedStepSupportFragment as content of Activity; no backstack entry is added so
+ * the activity will be dismissed when BACK key is pressed. The method is typically called in
+ * Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null,
+ * the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored
+ * by FragmentManager.
+ * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
+ *
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param activity The Activity to be used to insert GuidedstepFragment.
+ * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
+ * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
+ * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
+ * GuidedStepSupportFragment.
+ */
+ public static int addAsRoot(FragmentActivity activity, GuidedStepSupportFragment fragment, int id) {
+ // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
+ activity.getWindow().getDecorView();
+ FragmentManager fragmentManager = activity.getSupportFragmentManager();
+ if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
+ Log.w(TAG, "Fragment is already exists, likely calling "
+ + "addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
+ return -1;
+ }
+ FragmentTransaction ft = fragmentManager.beginTransaction();
+ fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
+ return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
+ }
+
+ /**
+ * Returns the current GuidedStepSupportFragment on the fragment transaction stack.
+ * @return The current GuidedStepSupportFragment, if any, on the fragment transaction stack.
+ */
+ public static GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(FragmentManager fm) {
+ Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
+ if (f instanceof GuidedStepSupportFragment) {
+ return (GuidedStepSupportFragment) f;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the GuidanceStylist that displays guidance information for the user.
+ * @return The GuidanceStylist for this fragment.
+ */
+ public GuidanceStylist getGuidanceStylist() {
+ return mGuidanceStylist;
+ }
+
+ /**
+ * Returns the GuidedActionsStylist that displays the actions the user may take.
+ * @return The GuidedActionsStylist for this fragment.
+ */
+ public GuidedActionsStylist getGuidedActionsStylist() {
+ return mActionsStylist;
+ }
+
+ /**
+ * Returns the list of button GuidedActions that the user may take in this fragment.
+ * @return The list of button GuidedActions for this fragment.
+ */
+ public List<GuidedAction> getButtonActions() {
+ return mButtonActions;
+ }
+
+ /**
+ * Find button GuidedAction by Id.
+ * @param id Id of the button action to search.
+ * @return GuidedAction object or null if not found.
+ */
+ public GuidedAction findButtonActionById(long id) {
+ int index = findButtonActionPositionById(id);
+ return index >= 0 ? mButtonActions.get(index) : null;
+ }
+
+ /**
+ * Find button GuidedAction position in array by Id.
+ * @param id Id of the button action to search.
+ * @return position of GuidedAction object in array or -1 if not found.
+ */
+ public int findButtonActionPositionById(long id) {
+ if (mButtonActions != null) {
+ for (int i = 0; i < mButtonActions.size(); i++) {
+ GuidedAction action = mButtonActions.get(i);
+ if (mButtonActions.get(i).getId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the GuidedActionsStylist that displays the button actions the user may take.
+ * @return The GuidedActionsStylist for this fragment.
+ */
+ public GuidedActionsStylist getGuidedButtonActionsStylist() {
+ return mButtonActionsStylist;
+ }
+
+ /**
+ * Sets the list of button GuidedActions that the user may take in this fragment.
+ * @param actions The list of button GuidedActions for this fragment.
+ */
+ public void setButtonActions(List<GuidedAction> actions) {
+ mButtonActions = actions;
+ if (mButtonAdapter != null) {
+ mButtonAdapter.setActions(mButtonActions);
+ }
+ }
+
+ /**
+ * Notify an button action has changed and update its UI.
+ * @param position Position of the button GuidedAction in array.
+ */
+ public void notifyButtonActionChanged(int position) {
+ if (mButtonAdapter != null) {
+ mButtonAdapter.notifyItemChanged(position);
+ }
+ }
+
+ /**
+ * Returns the view corresponding to the button action at the indicated position in the list of
+ * actions for this fragment.
+ * @param position The integer position of the button action of interest.
+ * @return The View corresponding to the button action at the indicated position, or null if
+ * that action is not currently onscreen.
+ */
+ public View getButtonActionItemView(int position) {
+ final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
+ .findViewHolderForPosition(position);
+ return holder == null ? null : holder.itemView;
+ }
+
+ /**
+ * Scrolls the action list to the position indicated, selecting that button action's view.
+ * @param position The integer position of the button action of interest.
+ */
+ public void setSelectedButtonActionPosition(int position) {
+ mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the position if the currently selected button GuidedAction.
+ * @return position The integer position of the currently selected button action.
+ */
+ public int getSelectedButtonActionPosition() {
+ return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
+ }
+
+ /**
+ * Returns the list of GuidedActions that the user may take in this fragment.
+ * @return The list of GuidedActions for this fragment.
+ */
+ public List<GuidedAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Find GuidedAction by Id.
+ * @param id Id of the action to search.
+ * @return GuidedAction object or null if not found.
+ */
+ public GuidedAction findActionById(long id) {
+ int index = findActionPositionById(id);
+ return index >= 0 ? mActions.get(index) : null;
+ }
+
+ /**
+ * Find GuidedAction position in array by Id.
+ * @param id Id of the action to search.
+ * @return position of GuidedAction object in array or -1 if not found.
+ */
+ public int findActionPositionById(long id) {
+ if (mActions != null) {
+ for (int i = 0; i < mActions.size(); i++) {
+ GuidedAction action = mActions.get(i);
+ if (mActions.get(i).getId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Sets the list of GuidedActions that the user may take in this fragment.
+ * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}.
+ *
+ * @param actions The list of GuidedActions for this fragment.
+ */
+ public void setActions(List<GuidedAction> actions) {
+ mActions = actions;
+ if (mAdapter != null) {
+ mAdapter.setActions(mActions);
+ }
+ }
+
+ /**
+ * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default
+ * GuidedStepSupportFragment uses
+ * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}.
+ * Sets it to null if app wants to refresh the whole list.
+ *
+ * @param diffCallback DiffCallback used in {@link #setActions(List)}.
+ */
+ public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+ mAdapter.setDiffCallback(diffCallback);
+ }
+
+ /**
+ * Notify an action has changed and update its UI.
+ * @param position Position of the GuidedAction in array.
+ */
+ public void notifyActionChanged(int position) {
+ if (mAdapter != null) {
+ mAdapter.notifyItemChanged(position);
+ }
+ }
+
+ /**
+ * Returns the view corresponding to the action at the indicated position in the list of
+ * actions for this fragment.
+ * @param position The integer position of the action of interest.
+ * @return The View corresponding to the action at the indicated position, or null if that
+ * action is not currently onscreen.
+ */
+ public View getActionItemView(int position) {
+ final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
+ .findViewHolderForPosition(position);
+ return holder == null ? null : holder.itemView;
+ }
+
+ /**
+ * Scrolls the action list to the position indicated, selecting that action's view.
+ * @param position The integer position of the action of interest.
+ */
+ public void setSelectedActionPosition(int position) {
+ mActionsStylist.getActionsGridView().setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the position if the currently selected GuidedAction.
+ * @return position The integer position of the currently selected action.
+ */
+ public int getSelectedActionPosition() {
+ return mActionsStylist.getActionsGridView().getSelectedPosition();
+ }
+
+ /**
+ * Called by Constructor to provide fragment transitions. The default implementation assigns
+ * transitions based on {@link #getUiStyle()}:
+ * <ul>
+ * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
+ * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
+ * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
+ * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
+ * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
+ * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
+ * enter transition.
+ * </ul>
+ * <p>
+ * The default implementation heavily relies on {@link GuidedActionsStylist} and
+ * {@link GuidanceStylist} layout, app may override this method when modifying the default
+ * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
+ * <p>
+ * TIP: because the fragment view is removed during fragment transition, in general app cannot
+ * use two Visibility transition together. Workaround is to create your own Visibility
+ * transition that controls multiple animators (e.g. slide and fade animation in one Transition
+ * class).
+ */
+ protected void onProvideFragmentTransitions() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ final int uiStyle = getUiStyle();
+ if (uiStyle == UI_STYLE_REPLACE) {
+ Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
+ TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
+ TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
+ true);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+
+ Object fade = TransitionHelper.createFadeTransition(
+ TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
+ TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
+ Object changeBounds = TransitionHelper.createChangeBounds(false);
+ Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(sharedElementTransition, fade);
+ TransitionHelper.addTransition(sharedElementTransition, changeBounds);
+ TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
+ } else if (uiStyle == UI_STYLE_ENTRANCE) {
+ if (entranceTransitionType == SLIDE_FROM_SIDE) {
+ Object fade = TransitionHelper.createFadeTransition(
+ TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
+ TransitionHelper.include(fade, R.id.guidedstep_background);
+ Object slideFromSide = TransitionHelper.createFadeAndShortSlide(
+ Gravity.END | Gravity.START);
+ TransitionHelper.include(slideFromSide, R.id.content_fragment);
+ TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
+ Object enterTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(enterTransition, fade);
+ TransitionHelper.addTransition(enterTransition, slideFromSide);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+ } else {
+ Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
+ Gravity.BOTTOM);
+ TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
+ Object enterTransition = TransitionHelper.createTransitionSet(false);
+ TransitionHelper.addTransition(enterTransition, slideFromBottom);
+ TransitionHelper.setEnterTransition(this, enterTransition);
+ }
+ // No shared element transition
+ TransitionHelper.setSharedElementEnterTransition(this, null);
+ } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
+ // for Activity root, we don't need enter transition, use activity transition
+ TransitionHelper.setEnterTransition(this, null);
+ // No shared element transition
+ TransitionHelper.setSharedElementEnterTransition(this, null);
+ }
+ // exitTransition is same for all style
+ Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
+ TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
+ TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
+ true);
+ TransitionHelper.setExitTransition(this, exitTransition);
+ }
+ }
+
+ /**
+ * Called by onCreateView to inflate background view. Default implementation loads view
+ * from {@link R.layout#lb_guidedstep_background} which holds a reference to
+ * guidedStepBackground.
+ * @param inflater LayoutInflater to load background view.
+ * @param container Parent view of background view.
+ * @param savedInstanceState
+ * @return Created background view or null if no background.
+ */
+ public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
+ }
+
+ /**
+ * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
+ * is first initialized. UI style is used to choose different fragment transition animations and
+ * determine if this is the first GuidedStepSupportFragment on backstack. In most cases app does not
+ * directly call this method, app calls helper function
+ * {@link #add(FragmentManager, GuidedStepSupportFragment, int)}. However if the app creates Fragment
+ * transaction and controls backstack by itself, it would need call setUiStyle() to select the
+ * fragment transition to use.
+ *
+ * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
+ * {@link #UI_STYLE_ENTRANCE}.
+ */
+ public void setUiStyle(int style) {
+ int oldStyle = getUiStyle();
+ Bundle arguments = getArguments();
+ boolean isNew = false;
+ if (arguments == null) {
+ arguments = new Bundle();
+ isNew = true;
+ }
+ arguments.putInt(EXTRA_UI_STYLE, style);
+ // call setArgument() will validate if the fragment is already added.
+ if (isNew) {
+ setArguments(arguments);
+ }
+ if (style != oldStyle) {
+ onProvideFragmentTransitions();
+ }
+ }
+
+ /**
+ * Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when
+ * fragment is first initialized. UI style is used to choose different fragment transition
+ * animations and determine if this is the first GuidedStepSupportFragment on backstack.
+ *
+ * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
+ * {@link #UI_STYLE_ENTRANCE}.
+ * @see #onProvideFragmentTransitions()
+ */
+ public int getUiStyle() {
+ Bundle b = getArguments();
+ if (b == null) return UI_STYLE_ENTRANCE;
+ return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (DEBUG) Log.v(TAG, "onCreate");
+ // Set correct transition from saved arguments.
+ onProvideFragmentTransitions();
+
+ ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
+ onCreateActions(actions, savedInstanceState);
+ if (savedInstanceState != null) {
+ onRestoreActions(actions, savedInstanceState);
+ }
+ setActions(actions);
+ ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
+ onCreateButtonActions(buttonActions, savedInstanceState);
+ if (savedInstanceState != null) {
+ onRestoreButtonActions(buttonActions, savedInstanceState);
+ }
+ setButtonActions(buttonActions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDestroyView() {
+ mGuidanceStylist.onDestroyView();
+ mActionsStylist.onDestroyView();
+ mButtonActionsStylist.onDestroyView();
+ mAdapter = null;
+ mSubAdapter = null;
+ mButtonAdapter = null;
+ mAdapterGroup = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (DEBUG) Log.v(TAG, "onCreateView");
+
+ resolveTheme();
+ inflater = getThemeInflater(inflater);
+
+ GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate(
+ R.layout.lb_guidedstep_fragment, container, false);
+
+ root.setFocusOutStart(isFocusOutStartAllowed());
+ root.setFocusOutEnd(isFocusOutEndAllowed());
+
+ ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
+ ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
+ ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
+
+ Guidance guidance = onCreateGuidance(savedInstanceState);
+ View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
+ guidanceContainer.addView(guidanceView);
+
+ View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
+ actionContainer.addView(actionsView);
+
+ View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
+ actionContainer.addView(buttonActionsView);
+
+ GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
+
+ @Override
+ public void onImeOpen() {
+ runImeAnimations(true);
+ }
+
+ @Override
+ public void onImeClose() {
+ runImeAnimations(false);
+ }
+
+ @Override
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ return GuidedStepSupportFragment.this.onGuidedActionEditedAndProceed(action);
+ }
+
+ @Override
+ public void onGuidedActionEditCanceled(GuidedAction action) {
+ GuidedStepSupportFragment.this.onGuidedActionEditCanceled(action);
+ }
+ };
+
+ mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ GuidedStepSupportFragment.this.onGuidedActionClicked(action);
+ if (isExpanded()) {
+ collapseAction(true);
+ } else if (action.hasSubActions() || action.hasEditableActivatorView()) {
+ expandAction(action, true);
+ }
+ }
+ }, this, mActionsStylist, false);
+ mButtonAdapter =
+ new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ GuidedStepSupportFragment.this.onGuidedActionClicked(action);
+ }
+ }, this, mButtonActionsStylist, false);
+ mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() {
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (mActionsStylist.isInExpandTransition()) {
+ return;
+ }
+ if (GuidedStepSupportFragment.this.onSubGuidedActionClicked(action)) {
+ collapseSubActions();
+ }
+ }
+ }, this, mActionsStylist, true);
+ mAdapterGroup = new GuidedActionAdapterGroup();
+ mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
+ mAdapterGroup.addAdpter(mSubAdapter, null);
+ mAdapterGroup.setEditListener(editListener);
+ mActionsStylist.setEditListener(editListener);
+
+ mActionsStylist.getActionsGridView().setAdapter(mAdapter);
+ if (mActionsStylist.getSubActionsGridView() != null) {
+ mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter);
+ }
+ mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
+ if (mButtonActions.size() == 0) {
+ // when there is no button actions, we don't need show the second panel, but keep
+ // the width zero to run ChangeBounds transition.
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+ buttonActionsView.getLayoutParams();
+ lp.weight = 0;
+ buttonActionsView.setLayoutParams(lp);
+ } else {
+ // when there are two actions panel, we need adjust the weight of action to
+ // guidedActionContentWidthWeightTwoPanels.
+ Context ctx = mThemeWrapper != null ? mThemeWrapper : getContext();
+ TypedValue typedValue = new TypedValue();
+ if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
+ typedValue, true)) {
+ View actionsRoot = root.findViewById(R.id.action_fragment_root);
+ float weight = typedValue.getFloat();
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
+ .getLayoutParams();
+ lp.weight = weight;
+ actionsRoot.setLayoutParams(lp);
+ }
+ }
+
+ // Add the background view.
+ View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState);
+ if (backgroundView != null) {
+ FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById(
+ R.id.guidedstep_background_view_root);
+ backgroundViewRoot.addView(backgroundView, 0);
+ }
+
+ return root;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getView().findViewById(R.id.action_fragment).requestFocus();
+ }
+
+ /**
+ * Get the key will be used to save GuidedAction with Fragment.
+ * @param action GuidedAction to get key.
+ * @return Key to save the GuidedAction.
+ */
+ final String getAutoRestoreKey(GuidedAction action) {
+ return EXTRA_ACTION_PREFIX + action.getId();
+ }
+
+ /**
+ * Get the key will be used to save GuidedAction with Fragment.
+ * @param action GuidedAction to get key.
+ * @return Key to save the GuidedAction.
+ */
+ final String getButtonAutoRestoreKey(GuidedAction action) {
+ return EXTRA_BUTTON_ACTION_PREFIX + action.getId();
+ }
+
+ final static boolean isSaveEnabled(GuidedAction action) {
+ return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID;
+ }
+
+ final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onSaveActions(List<GuidedAction> actions, Bundle outState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onSaveInstanceState(outState, getAutoRestoreKey(action));
+ }
+ }
+ }
+
+ final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) {
+ for (int i = 0, size = actions.size(); i < size; i++) {
+ GuidedAction action = actions.get(i);
+ if (isSaveEnabled(action)) {
+ action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action));
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ onSaveActions(mActions, outState);
+ onSaveButtonActions(mButtonActions, outState);
+ }
+
+ private static boolean isGuidedStepTheme(Context context) {
+ int resId = R.attr.guidedStepThemeFlag;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
+ return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
+ }
+
+ /**
+ * Convenient method to close GuidedStepSupportFragments on top of other content or finish Activity if
+ * GuidedStepSupportFragments were started in a separate activity. Pops all stack entries including
+ * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
+ * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepSupportFragment,
+ * int)} which sets up the stack entry name for finding which fragment we need to pop back to.
+ */
+ public void finishGuidedStepSupportFragments() {
+ final FragmentManager fragmentManager = getFragmentManager();
+ final int entryCount = fragmentManager.getBackStackEntryCount();
+ if (entryCount > 0) {
+ for (int i = entryCount - 1; i >= 0; i--) {
+ BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
+ if (isStackEntryUiStyleEntrance(entry.getName())) {
+ GuidedStepSupportFragment top = getCurrentGuidedStepSupportFragment(fragmentManager);
+ if (top != null) {
+ top.setUiStyle(UI_STYLE_ENTRANCE);
+ }
+ fragmentManager.popBackStackImmediate(entry.getId(),
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ return;
+ }
+ }
+ }
+ ActivityCompat.finishAfterTransition(getActivity());
+ }
+
+ /**
+ * Convenient method to pop to fragment with Given class.
+ * @param guidedStepFragmentClass Name of the Class of GuidedStepSupportFragment to pop to.
+ * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
+ */
+ public void popBackStackToGuidedStepSupportFragment(Class guidedStepFragmentClass, int flags) {
+ if (!GuidedStepSupportFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
+ return;
+ }
+ final FragmentManager fragmentManager = getFragmentManager();
+ final int entryCount = fragmentManager.getBackStackEntryCount();
+ String className = guidedStepFragmentClass.getName();
+ if (entryCount > 0) {
+ for (int i = entryCount - 1; i >= 0; i--) {
+ BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
+ String entryClassName = getGuidedStepSupportFragmentClassName(entry.getName());
+ if (className.equals(entryClassName)) {
+ fragmentManager.popBackStackImmediate(entry.getId(), flags);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if allows focus out of start edge of GuidedStepSupportFragment, false otherwise.
+ * Default value is false, the reason is to disable FocusFinder to find focusable views
+ * beneath content of GuidedStepSupportFragment. Subclass may override.
+ * @return True if allows focus out of start edge of GuidedStepSupportFragment.
+ */
+ public boolean isFocusOutStartAllowed() {
+ return false;
+ }
+
+ /**
+ * Returns true if allows focus out of end edge of GuidedStepSupportFragment, false otherwise.
+ * Default value is false, the reason is to disable FocusFinder to find focusable views
+ * beneath content of GuidedStepSupportFragment. Subclass may override.
+ * @return True if allows focus out of end edge of GuidedStepSupportFragment.
+ */
+ public boolean isFocusOutEndAllowed() {
+ return false;
+ }
+
+ /**
+ * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation.
+ * Currently we provide 2 different variations for animation - slide in from
+ * side (default) or bottom.
+ *
+ * Ideally we can retrieve the screen mode settings from the theme attribute
+ * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to
+ * determine the transition. But the fragment context to retrieve the theme
+ * isn't available on platform v23 or earlier.
+ *
+ * For now clients(subclasses) can call this method inside the constructor.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setEntranceTransitionType(int transitionType) {
+ this.entranceTransitionType = transitionType;
+ }
+
+ /**
+ * Opens the provided action in edit mode and raises ime. This can be
+ * used to programmatically skip the extra click required to go into edit mode. This method
+ * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
+ */
+ public void openInEditMode(GuidedAction action) {
+ mActionsStylist.openInEditMode(action);
+ }
+
+ private void resolveTheme() {
+ // Look up the guidedStepTheme in the currently specified theme. If it exists,
+ // replace the theme with its value.
+ Context context = getContext();
+ int theme = onProvideTheme();
+ if (theme == -1 && !isGuidedStepTheme(context)) {
+ // Look up the guidedStepTheme in the activity's currently specified theme. If it
+ // exists, replace the theme with its value.
+ int resId = R.attr.guidedStepTheme;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
+ if (found) {
+ ContextThemeWrapper themeWrapper =
+ new ContextThemeWrapper(context, typedValue.resourceId);
+ if (isGuidedStepTheme(themeWrapper)) {
+ mThemeWrapper = themeWrapper;
+ } else {
+ found = false;
+ mThemeWrapper = null;
+ }
+ }
+ if (!found) {
+ Log.e(TAG, "GuidedStepSupportFragment does not have an appropriate theme set.");
+ }
+ } else if (theme != -1) {
+ mThemeWrapper = new ContextThemeWrapper(context, theme);
+ }
+ }
+
+ private LayoutInflater getThemeInflater(LayoutInflater inflater) {
+ if (mThemeWrapper == null) {
+ return inflater;
+ } else {
+ return inflater.cloneInContext(mThemeWrapper);
+ }
+ }
+
+ private int getFirstCheckedAction() {
+ for (int i = 0, size = mActions.size(); i < size; i++) {
+ if (mActions.get(i).isChecked()) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ void runImeAnimations(boolean entering) {
+ ArrayList<Animator> animators = new ArrayList<Animator>();
+ if (entering) {
+ mGuidanceStylist.onImeAppearing(animators);
+ mActionsStylist.onImeAppearing(animators);
+ mButtonActionsStylist.onImeAppearing(animators);
+ } else {
+ mGuidanceStylist.onImeDisappearing(animators);
+ mActionsStylist.onImeDisappearing(animators);
+ mButtonActionsStylist.onImeDisappearing(animators);
+ }
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animators);
+ set.start();
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
new file mode 100644
index 0000000..08780a5
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
@@ -0,0 +1,309 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from HeadersSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DividerPresenter;
+import android.support.v17.leanback.widget.DividerRow;
+import android.support.v17.leanback.widget.FocusHighlightHelper;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowHeaderPresenter;
+import android.support.v17.leanback.widget.SectionRow;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * An fragment containing a list of row headers. Implementation must support three types of rows:
+ * <ul>
+ * <li>{@link DividerRow} rendered by {@link DividerPresenter}.</li>
+ * <li>{@link Row} rendered by {@link RowHeaderPresenter}.</li>
+ * <li>{@link SectionRow} rendered by {@link RowHeaderPresenter}.</li>
+ * </ul>
+ * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize
+ * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}.
+ * @deprecated use {@link HeadersSupportFragment}
+ */
+@Deprecated
+public class HeadersFragment extends BaseRowFragment {
+
+ /**
+ * Interface definition for a callback to be invoked when a header item is clicked.
+ * @deprecated use {@link HeadersSupportFragment}
+ */
+ @Deprecated
+ public interface OnHeaderClickedListener {
+ /**
+ * Called when a header item has been clicked.
+ *
+ * @param viewHolder Row ViewHolder object corresponding to the selected Header.
+ * @param row Row object corresponding to the selected Header.
+ */
+ void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a header item is selected.
+ * @deprecated use {@link HeadersSupportFragment}
+ */
+ @Deprecated
+ public interface OnHeaderViewSelectedListener {
+ /**
+ * Called when a header item has been selected.
+ *
+ * @param viewHolder Row ViewHolder object corresponding to the selected Header.
+ * @param row Row object corresponding to the selected Header.
+ */
+ void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row);
+ }
+
+ private OnHeaderViewSelectedListener mOnHeaderViewSelectedListener;
+ OnHeaderClickedListener mOnHeaderClickedListener;
+ private boolean mHeadersEnabled = true;
+ private boolean mHeadersGone = false;
+ private int mBackgroundColor;
+ private boolean mBackgroundColorSet;
+
+ private static final PresenterSelector sHeaderPresenter = new ClassPresenterSelector()
+ .addClassPresenter(DividerRow.class, new DividerPresenter())
+ .addClassPresenter(SectionRow.class,
+ new RowHeaderPresenter(R.layout.lb_section_header, false))
+ .addClassPresenter(Row.class, new RowHeaderPresenter(R.layout.lb_header));
+
+ public HeadersFragment() {
+ setPresenterSelector(sHeaderPresenter);
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter());
+ }
+
+ public void setOnHeaderClickedListener(OnHeaderClickedListener listener) {
+ mOnHeaderClickedListener = listener;
+ }
+
+ public void setOnHeaderViewSelectedListener(OnHeaderViewSelectedListener listener) {
+ mOnHeaderViewSelectedListener = listener;
+ }
+
+ @Override
+ VerticalGridView findGridViewFromRoot(View view) {
+ return (VerticalGridView) view.findViewById(R.id.browse_headers);
+ }
+
+ @Override
+ void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,
+ int position, int subposition) {
+ if (mOnHeaderViewSelectedListener != null) {
+ if (viewHolder != null && position >= 0) {
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) viewHolder;
+ mOnHeaderViewSelectedListener.onHeaderSelected(
+ (RowHeaderPresenter.ViewHolder) vh.getViewHolder(), (Row) vh.getItem());
+ } else {
+ mOnHeaderViewSelectedListener.onHeaderSelected(null, null);
+ }
+ }
+ }
+
+ private final ItemBridgeAdapter.AdapterListener mAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
+ View headerView = viewHolder.getViewHolder().view;
+ headerView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnHeaderClickedListener != null) {
+ mOnHeaderClickedListener.onHeaderClicked(
+ (RowHeaderPresenter.ViewHolder) viewHolder.getViewHolder(),
+ (Row) viewHolder.getItem());
+ }
+ }
+ });
+ if (mWrapper != null) {
+ viewHolder.itemView.addOnLayoutChangeListener(sLayoutChangeListener);
+ } else {
+ headerView.addOnLayoutChangeListener(sLayoutChangeListener);
+ }
+ }
+
+ };
+
+ static OnLayoutChangeListener sLayoutChangeListener = new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ v.setPivotX(v.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? v.getWidth() : 0);
+ v.setPivotY(v.getMeasuredHeight() / 2);
+ }
+ };
+
+ @Override
+ int getLayoutResourceId() {
+ return R.layout.lb_headers_fragment;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ final VerticalGridView listView = getVerticalGridView();
+ if (listView == null) {
+ return;
+ }
+ if (mBackgroundColorSet) {
+ listView.setBackgroundColor(mBackgroundColor);
+ updateFadingEdgeToBrandColor(mBackgroundColor);
+ } else {
+ Drawable d = listView.getBackground();
+ if (d instanceof ColorDrawable) {
+ updateFadingEdgeToBrandColor(((ColorDrawable) d).getColor());
+ }
+ }
+ updateListViewVisibility();
+ }
+
+ private void updateListViewVisibility() {
+ final VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ getView().setVisibility(mHeadersGone ? View.GONE : View.VISIBLE);
+ if (!mHeadersGone) {
+ if (mHeadersEnabled) {
+ listView.setChildrenVisibility(View.VISIBLE);
+ } else {
+ listView.setChildrenVisibility(View.INVISIBLE);
+ }
+ }
+ }
+ }
+
+ void setHeadersEnabled(boolean enabled) {
+ mHeadersEnabled = enabled;
+ updateListViewVisibility();
+ }
+
+ void setHeadersGone(boolean gone) {
+ mHeadersGone = gone;
+ updateListViewVisibility();
+ }
+
+ static class NoOverlappingFrameLayout extends FrameLayout {
+
+ public NoOverlappingFrameLayout(Context context) {
+ super(context);
+ }
+
+ /**
+ * Avoid creating hardware layer for header dock.
+ */
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+ }
+
+ // Wrapper needed because of conflict between RecyclerView's use of alpha
+ // for ADD animations, and RowHeaderPresenter's use of alpha for selected level.
+ final ItemBridgeAdapter.Wrapper mWrapper = new ItemBridgeAdapter.Wrapper() {
+ @Override
+ public void wrap(View wrapper, View wrapped) {
+ ((FrameLayout) wrapper).addView(wrapped);
+ }
+
+ @Override
+ public View createWrapper(View root) {
+ return new NoOverlappingFrameLayout(root.getContext());
+ }
+ };
+ @Override
+ void updateAdapter() {
+ super.updateAdapter();
+ ItemBridgeAdapter adapter = getBridgeAdapter();
+ adapter.setAdapterListener(mAdapterListener);
+ adapter.setWrapper(mWrapper);
+ }
+
+ void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ mBackgroundColorSet = true;
+
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().setBackgroundColor(mBackgroundColor);
+ updateFadingEdgeToBrandColor(mBackgroundColor);
+ }
+ }
+
+ private void updateFadingEdgeToBrandColor(int backgroundColor) {
+ View fadingView = getView().findViewById(R.id.fade_out_edge);
+ Drawable background = fadingView.getBackground();
+ if (background instanceof GradientDrawable) {
+ background.mutate();
+ ((GradientDrawable) background).setColors(
+ new int[] {Color.TRANSPARENT, backgroundColor});
+ }
+ }
+
+ @Override
+ public void onTransitionStart() {
+ super.onTransitionStart();
+ if (!mHeadersEnabled) {
+ // When enabling headers fragment, the RowHeaderView gets a focus but
+ // isShown() is still false because its parent is INVISIBLE, accessibility
+ // event is not sent.
+ // Workaround is: prevent focus to a child view during transition and put
+ // focus on it after transition is done.
+ final VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ if (listView.hasFocus()) {
+ listView.requestFocus();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionEnd() {
+ if (mHeadersEnabled) {
+ final VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ if (listView.hasFocus()) {
+ listView.requestFocus();
+ }
+ }
+ }
+ super.onTransitionEnd();
+ }
+
+ public boolean isScrolling() {
+ return getVerticalGridView().getScrollState()
+ != HorizontalGridView.SCROLL_STATE_IDLE;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java b/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
new file mode 100644
index 0000000..03d948b
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
@@ -0,0 +1,174 @@
+package android.support.v17.leanback.app;
+
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.Row;
+
+/**
+ * Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize
+ * {@link RowsFragment}. We use invisible rows to represent
+ * {@link android.support.v17.leanback.widget.DividerRow},
+ * {@link android.support.v17.leanback.widget.SectionRow} and
+ * {@link android.support.v17.leanback.widget.PageRow} in RowsFragment. In case we have an
+ * invisible row at the end of a RowsFragment, it creates a jumping effect as the layout manager
+ * thinks there are items even though they're invisible. This class takes care of filtering out
+ * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the
+ * bounds to reflect the latest data.
+ * {@link #detach()} must be called to release DataObserver from Adapter.
+ */
+class ListRowDataAdapter extends ObjectAdapter {
+ public static final int ON_ITEM_RANGE_CHANGED = 2;
+ public static final int ON_ITEM_RANGE_INSERTED = 4;
+ public static final int ON_ITEM_RANGE_REMOVED = 8;
+ public static final int ON_CHANGED = 16;
+
+ private final ObjectAdapter mAdapter;
+ int mLastVisibleRowIndex;
+ final DataObserver mDataObserver;
+
+ public ListRowDataAdapter(ObjectAdapter adapter) {
+ super(adapter.getPresenterSelector());
+ this.mAdapter = adapter;
+ initialize();
+
+ // If an user implements its own ObjectAdapter, notification corresponding to data
+ // updates can be batched e.g. remove, add might be followed by notifyRemove, notifyAdd.
+ // But underlying data would have changed during the notifyRemove call by the previous add
+ // operation. To handle this case, we use QueueBasedDataObserver which forces
+ // recyclerview to do a full data refresh after each update operation.
+ if (adapter.isImmediateNotifySupported()) {
+ mDataObserver = new SimpleDataObserver();
+ } else {
+ mDataObserver = new QueueBasedDataObserver();
+ }
+ attach();
+ }
+
+ void detach() {
+ mAdapter.unregisterObserver(mDataObserver);
+ }
+
+ void attach() {
+ initialize();
+ mAdapter.registerObserver(mDataObserver);
+ }
+
+ void initialize() {
+ mLastVisibleRowIndex = -1;
+ int i = mAdapter.size() - 1;
+ while (i >= 0) {
+ Row item = (Row) mAdapter.get(i);
+ if (item.isRenderedAsRowView()) {
+ mLastVisibleRowIndex = i;
+ break;
+ }
+ i--;
+ }
+ }
+
+ @Override
+ public int size() {
+ return mLastVisibleRowIndex + 1;
+ }
+
+ @Override
+ public Object get(int index) {
+ return mAdapter.get(index);
+ }
+
+ void doNotify(int eventType, int positionStart, int itemCount) {
+ switch (eventType) {
+ case ON_ITEM_RANGE_CHANGED:
+ notifyItemRangeChanged(positionStart, itemCount);
+ break;
+ case ON_ITEM_RANGE_INSERTED:
+ notifyItemRangeInserted(positionStart, itemCount);
+ break;
+ case ON_ITEM_RANGE_REMOVED:
+ notifyItemRangeRemoved(positionStart, itemCount);
+ break;
+ case ON_CHANGED:
+ notifyChanged();
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid event type " + eventType);
+ }
+ }
+
+ private class SimpleDataObserver extends DataObserver {
+
+ SimpleDataObserver() {
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ if (positionStart <= mLastVisibleRowIndex) {
+ onEventFired(ON_ITEM_RANGE_CHANGED, positionStart,
+ Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1));
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ if (positionStart <= mLastVisibleRowIndex) {
+ mLastVisibleRowIndex += itemCount;
+ onEventFired(ON_ITEM_RANGE_INSERTED, positionStart, itemCount);
+ return;
+ }
+
+ int lastVisibleRowIndex = mLastVisibleRowIndex;
+ initialize();
+ if (mLastVisibleRowIndex > lastVisibleRowIndex) {
+ int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex;
+ onEventFired(ON_ITEM_RANGE_INSERTED, lastVisibleRowIndex + 1, totalItems);
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ if (positionStart + itemCount - 1 < mLastVisibleRowIndex) {
+ mLastVisibleRowIndex -= itemCount;
+ onEventFired(ON_ITEM_RANGE_REMOVED, positionStart, itemCount);
+ return;
+ }
+
+ int lastVisibleRowIndex = mLastVisibleRowIndex;
+ initialize();
+ int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
+ if (totalItems > 0) {
+ onEventFired(ON_ITEM_RANGE_REMOVED,
+ Math.min(mLastVisibleRowIndex + 1, positionStart),
+ totalItems);
+ }
+ }
+
+ @Override
+ public void onChanged() {
+ initialize();
+ onEventFired(ON_CHANGED, -1, -1);
+ }
+
+ protected void onEventFired(int eventType, int positionStart, int itemCount) {
+ doNotify(eventType, positionStart, itemCount);
+ }
+ }
+
+
+ /**
+ * When using custom {@link ObjectAdapter}, it's possible that the user may make multiple
+ * changes to the underlying data at once. The notifications about those updates may be
+ * batched and the underlying data would have changed to reflect latest updates as opposed
+ * to intermediate changes. In order to force RecyclerView to refresh the view with access
+ * only to the final data, we call notifyChange().
+ */
+ private class QueueBasedDataObserver extends DataObserver {
+
+ QueueBasedDataObserver() {
+ }
+
+ @Override
+ public void onChanged() {
+ initialize();
+ notifyChanged();
+ }
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java b/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
new file mode 100644
index 0000000..f352c41
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
@@ -0,0 +1,1027 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from OnboardingSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * 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 android.support.v17.leanback.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.PagingIndicator;
+import android.app.Fragment;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An OnboardingFragment provides a common and simple way to build onboarding screen for
+ * applications.
+ * <p>
+ * <h3>Building the screen</h3>
+ * The view structure of onboarding screen is composed of the common parts and custom parts. The
+ * common parts are composed of icon, title, description and page navigator and the custom parts
+ * are composed of background, contents and foreground.
+ * <p>
+ * To build the screen views, the inherited class should override:
+ * <ul>
+ * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same
+ * size as the screen and the lowest z-order.</li>
+ * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in
+ * the content area at the center of the screen.</li>
+ * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same
+ * size as the screen and the highest z-order</li>
+ * </ul>
+ * <p>
+ * Each of these methods can return {@code null} if the application doesn't want to provide it.
+ * <p>
+ * <h3>Page information</h3>
+ * The onboarding screen may have several pages which explain the functionality of the application.
+ * The inherited class should provide the page information by overriding the methods:
+ * <p>
+ * <ul>
+ * <li>{@link #getPageCount} to provide the number of pages.</li>
+ * <li>{@link #getPageTitle} to provide the title of the page.</li>
+ * <li>{@link #getPageDescription} to provide the description of the page.</li>
+ * </ul>
+ * <p>
+ * Note that the information is used in {@link #onCreateView}, so should be initialized before
+ * calling {@code super.onCreateView}.
+ * <p>
+ * <h3>Animation</h3>
+ * Onboarding screen has three kinds of animations:
+ * <p>
+ * <h4>Logo Splash Animation</a></h4>
+ * When onboarding screen appears, the logo splash animation is played by default. The animation
+ * fades in the logo image, pauses in a few seconds and fades it out.
+ * <p>
+ * In most cases, the logo animation needs to be customized because the logo images of applications
+ * are different from each other, or some applications may want to show their own animations.
+ * <p>
+ * The logo animation can be customized in two ways:
+ * <ul>
+ * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show
+ * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>
+ * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the
+ * {@link Animator} object to run.</li>
+ * </ul>
+ * <p>
+ * If the inherited class provides neither the logo image nor the animation, the logo animation will
+ * be omitted.
+ * <h4>Page enter animation</h4>
+ * After logo animation finishes, page enter animation starts, which causes the header section -
+ * title and description views to fade and slide in. Users can override the default
+ * fade + slide animation by overriding {@link #onCreateTitleAnimator()} &
+ * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users
+ * can provide animation by overriding {@link #onCreateEnterAnimation}.
+ *
+ * <h4>Page change animation</h4>
+ * When the page changes, the default animations of the title and description are played. The
+ * inherited class can override {@link #onPageChanged} to start the custom animations.
+ * <p>
+ * <h3>Finishing the screen</h3>
+ * <p>
+ * If the user finishes the onboarding screen after navigating all the pages,
+ * {@link #onFinishFragment} is called. The inherited class can override this method to show another
+ * fragment or activity, or just remove this fragment.
+ * <p>
+ * <h3>Theming</h3>
+ * <p>
+ * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must
+ * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.
+ * Themes can be provided in one of three ways:
+ * <ul>
+ * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme
+ * that derives from it.</li>
+ * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
+ * existing Activity theme can have an entry added for the attribute
+ * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used
+ * by OnboardingFragment as an overlay to the Activity's theme.</li>
+ * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the
+ * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple
+ * Activities.</li>
+ * </ul>
+ * <p>
+ * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
+ * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not
+ * need to set the onboardingTheme attribute; if set, it will be ignored.)
+ *
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
+ * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
+ * @deprecated use {@link OnboardingSupportFragment}
+ */
+@Deprecated
+abstract public class OnboardingFragment extends Fragment {
+ private static final String TAG = "OnboardingF";
+ private static final boolean DEBUG = false;
+
+ private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;
+
+ private static final long HEADER_ANIMATION_DURATION_MS = 417;
+ private static final long DESCRIPTION_START_DELAY_MS = 33;
+ private static final long HEADER_APPEAR_DELAY_MS = 500;
+ private static final int SLIDE_DISTANCE = 60;
+
+ private static int sSlideDistance;
+
+ private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();
+ private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR =
+ new AccelerateInterpolator();
+
+ // Keys used to save and restore the states.
+ private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index";
+ private static final String KEY_LOGO_ANIMATION_FINISHED =
+ "leanback.onboarding.logo_animation_finished";
+ private static final String KEY_ENTER_ANIMATION_FINISHED =
+ "leanback.onboarding.enter_animation_finished";
+
+ private ContextThemeWrapper mThemeWrapper;
+
+ PagingIndicator mPageIndicator;
+ View mStartButton;
+ private ImageView mLogoView;
+ // Optional icon that can be displayed on top of the header section.
+ private ImageView mMainIconView;
+ private int mIconResourceId;
+
+ TextView mTitleView;
+ TextView mDescriptionView;
+
+ boolean mIsLtr;
+
+ // No need to save/restore the logo resource ID, because the logo animation will not appear when
+ // the fragment is restored.
+ private int mLogoResourceId;
+ boolean mLogoAnimationFinished;
+ boolean mEnterAnimationFinished;
+ int mCurrentPageIndex;
+
+ @ColorInt
+ private int mTitleViewTextColor = Color.TRANSPARENT;
+ private boolean mTitleViewTextColorSet;
+
+ @ColorInt
+ private int mDescriptionViewTextColor = Color.TRANSPARENT;
+ private boolean mDescriptionViewTextColorSet;
+
+ @ColorInt
+ private int mDotBackgroundColor = Color.TRANSPARENT;
+ private boolean mDotBackgroundColorSet;
+
+ @ColorInt
+ private int mArrowColor = Color.TRANSPARENT;
+ private boolean mArrowColorSet;
+
+ @ColorInt
+ private int mArrowBackgroundColor = Color.TRANSPARENT;
+ private boolean mArrowBackgroundColorSet;
+
+ private CharSequence mStartButtonText;
+ private boolean mStartButtonTextSet;
+
+
+ private AnimatorSet mAnimator;
+
+ private final OnClickListener mOnClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (!mLogoAnimationFinished) {
+ // Do not change page until the enter transition finishes.
+ return;
+ }
+ if (mCurrentPageIndex == getPageCount() - 1) {
+ onFinishFragment();
+ } else {
+ moveToNextPage();
+ }
+ }
+ };
+
+ private final OnKeyListener mOnKeyListener = new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (!mLogoAnimationFinished) {
+ // Ignore key event until the enter transition finishes.
+ return keyCode != KeyEvent.KEYCODE_BACK;
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (mCurrentPageIndex == 0) {
+ return false;
+ }
+ moveToPreviousPage();
+ return true;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mIsLtr) {
+ moveToPreviousPage();
+ } else {
+ moveToNextPage();
+ }
+ return true;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mIsLtr) {
+ moveToNextPage();
+ } else {
+ moveToPreviousPage();
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
+ /**
+ * Navigates to the previous page.
+ */
+ protected void moveToPreviousPage() {
+ if (!mLogoAnimationFinished) {
+ // Ignore if the logo enter transition is in progress.
+ return;
+ }
+ if (mCurrentPageIndex > 0) {
+ --mCurrentPageIndex;
+ onPageChangedInternal(mCurrentPageIndex + 1);
+ }
+ }
+
+ /**
+ * Navigates to the next page.
+ */
+ protected void moveToNextPage() {
+ if (!mLogoAnimationFinished) {
+ // Ignore if the logo enter transition is in progress.
+ return;
+ }
+ if (mCurrentPageIndex < getPageCount() - 1) {
+ ++mCurrentPageIndex;
+ onPageChangedInternal(mCurrentPageIndex - 1);
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, final ViewGroup container,
+ Bundle savedInstanceState) {
+ resolveTheme();
+ LayoutInflater localInflater = getThemeInflater(inflater);
+ final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
+ container, false);
+ mIsLtr = getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
+ mPageIndicator.setOnClickListener(mOnClickListener);
+ mPageIndicator.setOnKeyListener(mOnKeyListener);
+ mStartButton = view.findViewById(R.id.button_start);
+ mStartButton.setOnClickListener(mOnClickListener);
+ mStartButton.setOnKeyListener(mOnKeyListener);
+ mMainIconView = (ImageView) view.findViewById(R.id.main_icon);
+ mLogoView = (ImageView) view.findViewById(R.id.logo);
+ mTitleView = (TextView) view.findViewById(R.id.title);
+ mDescriptionView = (TextView) view.findViewById(R.id.description);
+
+ if (mTitleViewTextColorSet) {
+ mTitleView.setTextColor(mTitleViewTextColor);
+ }
+ if (mDescriptionViewTextColorSet) {
+ mDescriptionView.setTextColor(mDescriptionViewTextColor);
+ }
+ if (mDotBackgroundColorSet) {
+ mPageIndicator.setDotBackgroundColor(mDotBackgroundColor);
+ }
+ if (mArrowColorSet) {
+ mPageIndicator.setArrowColor(mArrowColor);
+ }
+ if (mArrowBackgroundColorSet) {
+ mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor);
+ }
+ if (mStartButtonTextSet) {
+ ((Button) mStartButton).setText(mStartButtonText);
+ }
+ final Context context = FragmentUtil.getContext(OnboardingFragment.this);
+ if (sSlideDistance == 0) {
+ sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
+ .getDisplayMetrics().scaledDensity);
+ }
+ view.requestFocus();
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (savedInstanceState == null) {
+ mCurrentPageIndex = 0;
+ mLogoAnimationFinished = false;
+ mEnterAnimationFinished = false;
+ mPageIndicator.onPageSelected(0, false);
+ view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getView().getViewTreeObserver().removeOnPreDrawListener(this);
+ if (!startLogoAnimation()) {
+ mLogoAnimationFinished = true;
+ onLogoAnimationFinished();
+ }
+ return true;
+ }
+ });
+ } else {
+ mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);
+ mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED);
+ mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED);
+ if (!mLogoAnimationFinished) {
+ // logo animation wasn't started or was interrupted when the activity was destroyed;
+ // restart it againl
+ if (!startLogoAnimation()) {
+ mLogoAnimationFinished = true;
+ onLogoAnimationFinished();
+ }
+ } else {
+ onLogoAnimationFinished();
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);
+ outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished);
+ outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished);
+ }
+
+ /**
+ * Sets the text color for TitleView. If not set, the default textColor set in style
+ * referenced by attr {@link R.attr#onboardingTitleStyle} will be used.
+ * @param color the color to use as the text color for TitleView
+ */
+ public void setTitleViewTextColor(@ColorInt int color) {
+ mTitleViewTextColor = color;
+ mTitleViewTextColorSet = true;
+ if (mTitleView != null) {
+ mTitleView.setTextColor(color);
+ }
+ }
+
+ /**
+ * Returns the text color of TitleView if it's set through
+ * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.
+ */
+ @ColorInt
+ public final int getTitleViewTextColor() {
+ return mTitleViewTextColor;
+ }
+
+ /**
+ * Sets the text color for DescriptionView. If not set, the default textColor set in style
+ * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used.
+ * @param color the color to use as the text color for DescriptionView
+ */
+ public void setDescriptionViewTextColor(@ColorInt int color) {
+ mDescriptionViewTextColor = color;
+ mDescriptionViewTextColorSet = true;
+ if (mDescriptionView != null) {
+ mDescriptionView.setTextColor(color);
+ }
+ }
+
+ /**
+ * Returns the text color of DescriptionView if it's set through
+ * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.
+ */
+ @ColorInt
+ public final int getDescriptionViewTextColor() {
+ return mDescriptionViewTextColor;
+ }
+ /**
+ * Sets the background color of the dots. If not set, the default color from attr
+ * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used.
+ * @param color the color to use for dot backgrounds
+ */
+ public void setDotBackgroundColor(@ColorInt int color) {
+ mDotBackgroundColor = color;
+ mDotBackgroundColorSet = true;
+ if (mPageIndicator != null) {
+ mPageIndicator.setDotBackgroundColor(color);
+ }
+ }
+
+ /**
+ * Returns the background color of the dot if it's set through
+ * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.
+ */
+ @ColorInt
+ public final int getDotBackgroundColor() {
+ return mDotBackgroundColor;
+ }
+
+ /**
+ * Sets the color of the arrow. This color will supersede the color set in the theme attribute
+ * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the
+ * arrow will have its original bitmap color.
+ *
+ * @param color the color to use for arrow background
+ */
+ public void setArrowColor(@ColorInt int color) {
+ mArrowColor = color;
+ mArrowColorSet = true;
+ if (mPageIndicator != null) {
+ mPageIndicator.setArrowColor(color);
+ }
+ }
+
+ /**
+ * Returns the color of the arrow if it's set through
+ * {@link #setArrowColor(int)}. If no color was set, transparent is returned.
+ */
+ @ColorInt
+ public final int getArrowColor() {
+ return mArrowColor;
+ }
+
+ /**
+ * Sets the background color of the arrow. If not set, the default color from attr
+ * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used.
+ * @param color the color to use for arrow background
+ */
+ public void setArrowBackgroundColor(@ColorInt int color) {
+ mArrowBackgroundColor = color;
+ mArrowBackgroundColorSet = true;
+ if (mPageIndicator != null) {
+ mPageIndicator.setArrowBackgroundColor(color);
+ }
+ }
+
+ /**
+ * Returns the background color of the arrow if it's set through
+ * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.
+ */
+ @ColorInt
+ public final int getArrowBackgroundColor() {
+ return mArrowBackgroundColor;
+ }
+
+ /**
+ * Returns the start button text if it's set through
+ * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
+ */
+ public final CharSequence getStartButtonText() {
+ return mStartButtonText;
+ }
+
+ /**
+ * Sets the text on the start button text. If not set, the default text set in
+ * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used.
+ *
+ * @param text the start button text
+ */
+ public void setStartButtonText(CharSequence text) {
+ mStartButtonText = text;
+ mStartButtonTextSet = true;
+ if (mStartButton != null) {
+ ((Button) mStartButton).setText(mStartButtonText);
+ }
+ }
+
+ /**
+ * Returns the theme used for styling the fragment. The default returns -1, indicating that the
+ * host Activity's theme should be used.
+ *
+ * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host
+ * Activity's theme.
+ */
+ public int onProvideTheme() {
+ return -1;
+ }
+
+ private void resolveTheme() {
+ final Context context = FragmentUtil.getContext(OnboardingFragment.this);
+ int theme = onProvideTheme();
+ if (theme == -1) {
+ // Look up the onboardingTheme in the activity's currently specified theme. If it
+ // exists, wrap the theme with its value.
+ int resId = R.attr.onboardingTheme;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
+ if (found) {
+ mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
+ }
+ } else {
+ mThemeWrapper = new ContextThemeWrapper(context, theme);
+ }
+ }
+
+ private LayoutInflater getThemeInflater(LayoutInflater inflater) {
+ return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);
+ }
+
+ /**
+ * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo
+ * splash animation will be played.
+ *
+ * @param id The resource ID of the logo image.
+ */
+ public final void setLogoResourceId(int id) {
+ mLogoResourceId = id;
+ }
+
+ /**
+ * Returns the resource ID of the splash logo image.
+ *
+ * @return The resource ID of the splash logo image.
+ */
+ public final int getLogoResourceId() {
+ return mLogoResourceId;
+ }
+
+ /**
+ * Called to have the inherited class create its own logo animation.
+ * <p>
+ * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.
+ * If this returns {@code null}, the logo animation is skipped.
+ *
+ * @return The {@link Animator} object which runs the logo animation.
+ */
+ @Nullable
+ protected Animator onCreateLogoAnimation() {
+ return null;
+ }
+
+ boolean startLogoAnimation() {
+ final Context context = FragmentUtil.getContext(OnboardingFragment.this);
+ if (context == null) {
+ return false;
+ }
+ Animator animator = null;
+ if (mLogoResourceId != 0) {
+ mLogoView.setVisibility(View.VISIBLE);
+ mLogoView.setImageResource(mLogoResourceId);
+ Animator inAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_logo_enter);
+ Animator outAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_logo_exit);
+ outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
+ AnimatorSet logoAnimator = new AnimatorSet();
+ logoAnimator.playSequentially(inAnimator, outAnimator);
+ logoAnimator.setTarget(mLogoView);
+ animator = logoAnimator;
+ } else {
+ animator = onCreateLogoAnimation();
+ }
+ if (animator != null) {
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (context != null) {
+ mLogoAnimationFinished = true;
+ onLogoAnimationFinished();
+ }
+ }
+ });
+ animator.start();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called to have the inherited class create its enter animation. The start animation runs after
+ * logo animation ends.
+ *
+ * @return The {@link Animator} object which runs the page enter animation.
+ */
+ @Nullable
+ protected Animator onCreateEnterAnimation() {
+ return null;
+ }
+
+
+ /**
+ * Hides the logo view and makes other fragment views visible. Also initializes the texts for
+ * Title and Description views.
+ */
+ void hideLogoView() {
+ mLogoView.setVisibility(View.GONE);
+
+ if (mIconResourceId != 0) {
+ mMainIconView.setImageResource(mIconResourceId);
+ mMainIconView.setVisibility(View.VISIBLE);
+ }
+
+ View container = getView();
+ // Create custom views.
+ LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
+ FragmentUtil.getContext(OnboardingFragment.this)));
+ ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
+ R.id.background_container);
+ View background = onCreateBackgroundView(inflater, backgroundContainer);
+ if (background != null) {
+ backgroundContainer.setVisibility(View.VISIBLE);
+ backgroundContainer.addView(background);
+ }
+ ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);
+ View content = onCreateContentView(inflater, contentContainer);
+ if (content != null) {
+ contentContainer.setVisibility(View.VISIBLE);
+ contentContainer.addView(content);
+ }
+ ViewGroup foregroundContainer = (ViewGroup) container.findViewById(
+ R.id.foreground_container);
+ View foreground = onCreateForegroundView(inflater, foregroundContainer);
+ if (foreground != null) {
+ foregroundContainer.setVisibility(View.VISIBLE);
+ foregroundContainer.addView(foreground);
+ }
+ // Make views visible which were invisible while logo animation is running.
+ container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);
+ container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);
+ if (getPageCount() > 1) {
+ mPageIndicator.setPageCount(getPageCount());
+ mPageIndicator.onPageSelected(mCurrentPageIndex, false);
+ }
+ if (mCurrentPageIndex == getPageCount() - 1) {
+ mStartButton.setVisibility(View.VISIBLE);
+ } else {
+ mPageIndicator.setVisibility(View.VISIBLE);
+ }
+ // Header views.
+ mTitleView.setText(getPageTitle(mCurrentPageIndex));
+ mDescriptionView.setText(getPageDescription(mCurrentPageIndex));
+ }
+
+ /**
+ * Called immediately after the logo animation is complete or no logo animation is specified.
+ * This method can also be called when the activity is recreated, i.e. when no logo animation
+ * are performed.
+ * By default, this method will hide the logo view and start the entrance animation for this
+ * fragment.
+ * Overriding subclasses can provide their own data loading logic as to when the entrance
+ * animation should be executed.
+ */
+ protected void onLogoAnimationFinished() {
+ startEnterAnimation(false);
+ }
+
+ /**
+ * Called to start entrance transition. This can be called by subclasses when the logo animation
+ * and data loading is complete. If force flag is set to false, it will only start the animation
+ * if it's not already done yet. Otherwise, it will always start the enter animation. In both
+ * cases, the logo view will hide and the rest of fragment views become visible after this call.
+ *
+ * @param force {@code true} if enter animation has to be performed regardless of whether it's
+ * been done in the past, {@code false} otherwise
+ */
+ protected final void startEnterAnimation(boolean force) {
+ final Context context = FragmentUtil.getContext(OnboardingFragment.this);
+ if (context == null) {
+ return;
+ }
+ hideLogoView();
+ if (mEnterAnimationFinished && !force) {
+ return;
+ }
+ List<Animator> animators = new ArrayList<>();
+ Animator animator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_page_indicator_enter);
+ animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
+ animators.add(animator);
+
+ animator = onCreateTitleAnimator();
+ if (animator != null) {
+ // Header title.
+ animator.setTarget(mTitleView);
+ animators.add(animator);
+ }
+
+ animator = onCreateDescriptionAnimator();
+ if (animator != null) {
+ // Header description.
+ animator.setTarget(mDescriptionView);
+ animators.add(animator);
+ }
+
+ // Customized animation by the inherited class.
+ Animator customAnimator = onCreateEnterAnimation();
+ if (customAnimator != null) {
+ animators.add(customAnimator);
+ }
+
+ // Return if we don't have any animations.
+ if (animators.isEmpty()) {
+ return;
+ }
+ mAnimator = new AnimatorSet();
+ mAnimator.playTogether(animators);
+ mAnimator.start();
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mEnterAnimationFinished = true;
+ }
+ });
+ // Search focus and give the focus to the appropriate child which has become visible.
+ getView().requestFocus();
+ }
+
+ /**
+ * Provides the entry animation for description view. This allows users to override the
+ * default fade and slide animation. Returning null will disable the animation.
+ */
+ protected Animator onCreateDescriptionAnimator() {
+ return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
+ R.animator.lb_onboarding_description_enter);
+ }
+
+ /**
+ * Provides the entry animation for title view. This allows users to override the
+ * default fade and slide animation. Returning null will disable the animation.
+ */
+ protected Animator onCreateTitleAnimator() {
+ return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
+ R.animator.lb_onboarding_title_enter);
+ }
+
+ /**
+ * Returns whether the logo enter animation is finished.
+ *
+ * @return {@code true} if the logo enter transition is finished, {@code false} otherwise
+ */
+ protected final boolean isLogoAnimationFinished() {
+ return mLogoAnimationFinished;
+ }
+
+ /**
+ * Returns the page count.
+ *
+ * @return The page count.
+ */
+ abstract protected int getPageCount();
+
+ /**
+ * Returns the title of the given page.
+ *
+ * @param pageIndex The page index.
+ *
+ * @return The title of the page.
+ */
+ abstract protected CharSequence getPageTitle(int pageIndex);
+
+ /**
+ * Returns the description of the given page.
+ *
+ * @param pageIndex The page index.
+ *
+ * @return The description of the page.
+ */
+ abstract protected CharSequence getPageDescription(int pageIndex);
+
+ /**
+ * Returns the index of the current page.
+ *
+ * @return The index of the current page.
+ */
+ protected final int getCurrentPageIndex() {
+ return mCurrentPageIndex;
+ }
+
+ /**
+ * Called to have the inherited class create background view. This is optional and the fragment
+ * which doesn't have the background view can return {@code null}. This is called inside
+ * {@link #onCreateView}.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate the views,
+ * @param container The parent view that the additional views are attached to.The fragment
+ * should not add the view by itself.
+ *
+ * @return The background view for the onboarding screen, or {@code null}.
+ */
+ @Nullable
+ abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container);
+
+ /**
+ * Called to have the inherited class create content view. This is optional and the fragment
+ * which doesn't have the content view can return {@code null}. This is called inside
+ * {@link #onCreateView}.
+ *
+ * <p>The content view would be located at the center of the screen.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate the views,
+ * @param container The parent view that the additional views are attached to.The fragment
+ * should not add the view by itself.
+ *
+ * @return The content view for the onboarding screen, or {@code null}.
+ */
+ @Nullable
+ abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container);
+
+ /**
+ * Called to have the inherited class create foreground view. This is optional and the fragment
+ * which doesn't need the foreground view can return {@code null}. This is called inside
+ * {@link #onCreateView}.
+ *
+ * <p>This foreground view would have the highest z-order.
+ *
+ * @param inflater The LayoutInflater object that can be used to inflate the views,
+ * @param container The parent view that the additional views are attached to.The fragment
+ * should not add the view by itself.
+ *
+ * @return The foreground view for the onboarding screen, or {@code null}.
+ */
+ @Nullable
+ abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
+
+ /**
+ * Called when the onboarding flow finishes.
+ */
+ protected void onFinishFragment() { }
+
+ /**
+ * Called when the page changes.
+ */
+ private void onPageChangedInternal(int previousPage) {
+ if (mAnimator != null) {
+ mAnimator.end();
+ }
+ mPageIndicator.onPageSelected(mCurrentPageIndex, true);
+
+ List<Animator> animators = new ArrayList<>();
+ // Header animation
+ Animator fadeAnimator = null;
+ if (previousPage < getCurrentPageIndex()) {
+ // sliding to left
+ animators.add(createAnimator(mTitleView, false, Gravity.START, 0));
+ animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,
+ DESCRIPTION_START_DELAY_MS));
+ animators.add(createAnimator(mTitleView, true, Gravity.END,
+ HEADER_APPEAR_DELAY_MS));
+ animators.add(createAnimator(mDescriptionView, true, Gravity.END,
+ HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
+ } else {
+ // sliding to right
+ animators.add(createAnimator(mTitleView, false, Gravity.END, 0));
+ animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,
+ DESCRIPTION_START_DELAY_MS));
+ animators.add(createAnimator(mTitleView, true, Gravity.START,
+ HEADER_APPEAR_DELAY_MS));
+ animators.add(createAnimator(mDescriptionView, true, Gravity.START,
+ HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
+ }
+ final int currentPageIndex = getCurrentPageIndex();
+ fadeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mTitleView.setText(getPageTitle(currentPageIndex));
+ mDescriptionView.setText(getPageDescription(currentPageIndex));
+ }
+ });
+
+ final Context context = FragmentUtil.getContext(OnboardingFragment.this);
+ // Animator for switching between page indicator and button.
+ if (getCurrentPageIndex() == getPageCount() - 1) {
+ mStartButton.setVisibility(View.VISIBLE);
+ Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_page_indicator_fade_out);
+ navigatorFadeOutAnimator.setTarget(mPageIndicator);
+ navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mPageIndicator.setVisibility(View.GONE);
+ }
+ });
+ animators.add(navigatorFadeOutAnimator);
+ Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_start_button_fade_in);
+ buttonFadeInAnimator.setTarget(mStartButton);
+ animators.add(buttonFadeInAnimator);
+ } else if (previousPage == getPageCount() - 1) {
+ mPageIndicator.setVisibility(View.VISIBLE);
+ Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_page_indicator_fade_in);
+ navigatorFadeInAnimator.setTarget(mPageIndicator);
+ animators.add(navigatorFadeInAnimator);
+ Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
+ R.animator.lb_onboarding_start_button_fade_out);
+ buttonFadeOutAnimator.setTarget(mStartButton);
+ buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStartButton.setVisibility(View.GONE);
+ }
+ });
+ animators.add(buttonFadeOutAnimator);
+ }
+ mAnimator = new AnimatorSet();
+ mAnimator.playTogether(animators);
+ mAnimator.start();
+ onPageChanged(mCurrentPageIndex, previousPage);
+ }
+
+ /**
+ * Called when the page has been changed.
+ *
+ * @param newPage The new page.
+ * @param previousPage The previous page.
+ */
+ protected void onPageChanged(int newPage, int previousPage) { }
+
+ private Animator createAnimator(View view, boolean fadeIn, int slideDirection,
+ long startDelay) {
+ boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
+ boolean slideRight = (isLtr && slideDirection == Gravity.END)
+ || (!isLtr && slideDirection == Gravity.START)
+ || slideDirection == Gravity.RIGHT;
+ Animator fadeAnimator;
+ Animator slideAnimator;
+ if (fadeIn) {
+ fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);
+ slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
+ slideRight ? sSlideDistance : -sSlideDistance, 0);
+ fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
+ slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
+ } else {
+ fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);
+ slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,
+ slideRight ? sSlideDistance : -sSlideDistance);
+ fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
+ slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
+ }
+ fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
+ fadeAnimator.setTarget(view);
+ slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
+ slideAnimator.setTarget(view);
+ AnimatorSet animator = new AnimatorSet();
+ animator.playTogether(fadeAnimator, slideAnimator);
+ if (startDelay > 0) {
+ animator.setStartDelay(startDelay);
+ }
+ return animator;
+ }
+
+ /**
+ * Sets the resource id for the main icon.
+ */
+ public final void setIconResouceId(int resourceId) {
+ this.mIconResourceId = resourceId;
+ if (mMainIconView != null) {
+ mMainIconView.setImageResource(resourceId);
+ mMainIconView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the resource id of the main icon.
+ */
+ public final int getIconResourceId() {
+ return mIconResourceId;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java b/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PermissionHelper.java b/leanback/src/android/support/v17/leanback/app/PermissionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PermissionHelper.java
rename to leanback/src/android/support/v17/leanback/app/PermissionHelper.java
diff --git a/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
new file mode 100644
index 0000000..e2e6be4
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -0,0 +1,1178 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.animation.LogAccelerateInterpolator;
+import android.support.v17.leanback.animation.LogDecelerateInterpolator;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.ItemAlignmentFacet;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+
+/**
+ * A fragment for displaying playback controls and related content.
+ *
+ * <p>
+ * A PlaybackFragment renders the elements of its {@link ObjectAdapter} as a set
+ * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
+ * of {@link RowPresenter}.
+ * </p>
+ * <p>
+ * A playback row is a row rendered by {@link PlaybackRowPresenter}.
+ * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter.
+ * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it.
+ * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are
+ * optional, app can pass playback row and PlaybackRowPresenter in the adapter using
+ * {@link #setAdapter(ObjectAdapter)}.
+ * </p>
+ * <p>
+ * Auto hide controls upon playing: best practice is calling
+ * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
+ * be cancelled upon {@link #tickle()} triggered by input event.
+ * </p>
+ * @deprecated use {@link PlaybackSupportFragment}
+ */
+@Deprecated
+public class PlaybackFragment extends Fragment {
+ static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
+
+ /**
+ * No background.
+ */
+ public static final int BG_NONE = 0;
+
+ /**
+ * A dark translucent background.
+ */
+ public static final int BG_DARK = 1;
+ PlaybackGlueHost.HostCallback mHostCallback;
+
+ PlaybackSeekUi.Client mSeekUiClient;
+ boolean mInSeek;
+ ProgressBarManager mProgressBarManager = new ProgressBarManager();
+
+ /**
+ * Resets the focus on the button in the middle of control row.
+ * @hide
+ */
+ public void resetFocus() {
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
+ .findViewHolderForAdapterPosition(0);
+ if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
+ ((PlaybackRowPresenter) vh.getPresenter()).onReappear(
+ (RowPresenter.ViewHolder) vh.getViewHolder());
+ }
+ }
+
+ private class SetSelectionRunnable implements Runnable {
+ int mPosition;
+ boolean mSmooth = true;
+
+ @Override
+ public void run() {
+ if (mRowsFragment == null) {
+ return;
+ }
+ mRowsFragment.setSelectedPosition(mPosition, mSmooth);
+ }
+ }
+
+ /**
+ * A light translucent background.
+ */
+ public static final int BG_LIGHT = 2;
+ RowsFragment mRowsFragment;
+ ObjectAdapter mAdapter;
+ PlaybackRowPresenter mPresenter;
+ Row mRow;
+ BaseOnItemViewSelectedListener mExternalItemSelectedListener;
+ BaseOnItemViewClickedListener mExternalItemClickedListener;
+ BaseOnItemViewClickedListener mPlaybackItemClickedListener;
+
+ private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
+ new BaseOnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mPlaybackItemClickedListener != null
+ && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
+ mPlaybackItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ if (mExternalItemClickedListener != null) {
+ mExternalItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
+ private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
+ new BaseOnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mExternalItemSelectedListener != null) {
+ mExternalItemSelectedListener.onItemSelected(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
+ private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
+
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Listener allowing the application to receive notification of fade in and/or fade out
+ * completion events.
+ * @hide
+ * @deprecated use {@link PlaybackSupportFragment}
+ */
+ @Deprecated
+ public static class OnFadeCompleteListener {
+ public void onFadeInComplete() {
+ }
+
+ public void onFadeOutComplete() {
+ }
+ }
+
+ private static final String TAG = "PlaybackFragment";
+ private static final boolean DEBUG = false;
+ private static final int ANIMATION_MULTIPLIER = 1;
+
+ private static int START_FADE_OUT = 1;
+
+ // Fading status
+ private static final int IDLE = 0;
+ private static final int ANIMATING = 1;
+
+ int mPaddingBottom;
+ int mOtherRowsCenterToBottom;
+ View mRootView;
+ View mBackgroundView;
+ int mBackgroundType = BG_DARK;
+ int mBgDarkColor;
+ int mBgLightColor;
+ int mShowTimeMs;
+ int mMajorFadeTranslateY, mMinorFadeTranslateY;
+ int mAnimationTranslateY;
+ OnFadeCompleteListener mFadeCompleteListener;
+ View.OnKeyListener mInputEventHandler;
+ boolean mFadingEnabled = true;
+ boolean mControlVisibleBeforeOnCreateView = true;
+ boolean mControlVisible = true;
+ int mBgAlpha;
+ ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
+ ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
+ ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator;
+
+ private final Animator.AnimatorListener mFadeListener =
+ new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ enableVerticalGridAnimations(false);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
+ if (mBgAlpha > 0) {
+ enableVerticalGridAnimations(true);
+ if (mFadeCompleteListener != null) {
+ mFadeCompleteListener.onFadeInComplete();
+ }
+ } else {
+ VerticalGridView verticalView = getVerticalGridView();
+ // reset focus to the primary actions only if the selected row was the controls row
+ if (verticalView != null && verticalView.getSelectedPosition() == 0) {
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ verticalView.findViewHolderForAdapterPosition(0);
+ if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
+ ((PlaybackRowPresenter)vh.getPresenter()).onReappear(
+ (RowPresenter.ViewHolder) vh.getViewHolder());
+ }
+ }
+ if (mFadeCompleteListener != null) {
+ mFadeCompleteListener.onFadeOutComplete();
+ }
+ }
+ }
+ };
+
+ public PlaybackFragment() {
+ mProgressBarManager.setInitialDelay(500);
+ }
+
+ VerticalGridView getVerticalGridView() {
+ if (mRowsFragment == null) {
+ return null;
+ }
+ return mRowsFragment.getVerticalGridView();
+ }
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == START_FADE_OUT && mFadingEnabled) {
+ hideControlsOverlay(true);
+ }
+ }
+ };
+
+ private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener =
+ new VerticalGridView.OnTouchInterceptListener() {
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return onInterceptInputEvent(event);
+ }
+ };
+
+ private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener =
+ new VerticalGridView.OnKeyInterceptListener() {
+ @Override
+ public boolean onInterceptKeyEvent(KeyEvent event) {
+ return onInterceptInputEvent(event);
+ }
+ };
+
+ private void setBgAlpha(int alpha) {
+ mBgAlpha = alpha;
+ if (mBackgroundView != null) {
+ mBackgroundView.getBackground().setAlpha(alpha);
+ }
+ }
+
+ private void enableVerticalGridAnimations(boolean enable) {
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().setAnimateChildLayout(enable);
+ }
+ }
+
+ /**
+ * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.
+ * If enabled and fragment is resumed, the view will fade out after a time period.
+ * {@link #tickle()} will kill the timer, next time fragment is resumed,
+ * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.
+ */
+ public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+ if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled);
+ if (enabled != mFadingEnabled) {
+ mFadingEnabled = enabled;
+ if (isResumed() && getView().hasFocus()) {
+ showControlsOverlay(true);
+ if (enabled) {
+ // StateGraph 7->2 5->2
+ startFadeTimer();
+ } else {
+ // StateGraph 4->5 2->5
+ stopFadeTimer();
+ }
+ } else {
+ // StateGraph 6->1 1->6
+ }
+ }
+ }
+
+ /**
+ * Returns true if controls will be auto hidden after a delay when fragment is resumed.
+ */
+ public boolean isControlsOverlayAutoHideEnabled() {
+ return mFadingEnabled;
+ }
+
+ /**
+ * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}
+ */
+ @Deprecated
+ public void setFadingEnabled(boolean enabled) {
+ setControlsOverlayAutoHideEnabled(enabled);
+ }
+
+ /**
+ * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}
+ */
+ @Deprecated
+ public boolean isFadingEnabled() {
+ return isControlsOverlayAutoHideEnabled();
+ }
+
+ /**
+ * Sets the listener to be called when fade in or out has completed.
+ * @hide
+ */
+ public void setFadeCompleteListener(OnFadeCompleteListener listener) {
+ mFadeCompleteListener = listener;
+ }
+
+ /**
+ * Returns the listener to be called when fade in or out has completed.
+ * @hide
+ */
+ public OnFadeCompleteListener getFadeCompleteListener() {
+ return mFadeCompleteListener;
+ }
+
+ /**
+ * Sets the input event handler.
+ */
+ public final void setOnKeyInterceptListener(View.OnKeyListener handler) {
+ mInputEventHandler = handler;
+ }
+
+ /**
+ * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will
+ * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When
+ * next time fragment is resumed, the timer will be started again if
+ * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call
+ * this method, tickling on input events is handled by the fragment.
+ */
+ public void tickle() {
+ if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
+ //StateGraph 2->4
+ stopFadeTimer();
+ showControlsOverlay(true);
+ }
+
+ private boolean onInterceptInputEvent(InputEvent event) {
+ final boolean controlsHidden = !mControlVisible;
+ if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event);
+ boolean consumeEvent = false;
+ int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+ int keyAction = 0;
+
+ if (event instanceof KeyEvent) {
+ keyCode = ((KeyEvent) event).getKeyCode();
+ keyAction = ((KeyEvent) event).getAction();
+ if (mInputEventHandler != null) {
+ consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);
+ }
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // Event may be consumed; regardless, if controls are hidden then these keys will
+ // bring up the controls.
+ if (controlsHidden) {
+ consumeEvent = true;
+ }
+ if (keyAction == KeyEvent.ACTION_DOWN) {
+ tickle();
+ }
+ break;
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_ESCAPE:
+ if (mInSeek) {
+ // when in seek, the SeekUi will handle the BACK.
+ return false;
+ }
+ // If controls are not hidden, back will be consumed to fade
+ // them out (even if the key was consumed by the handler).
+ if (!controlsHidden) {
+ consumeEvent = true;
+
+ if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {
+ hideControlsOverlay(true);
+ }
+ }
+ break;
+ default:
+ if (consumeEvent) {
+ if (keyAction == KeyEvent.ACTION_DOWN) {
+ tickle();
+ }
+ }
+ }
+ return consumeEvent;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // controls view are initially visible, make it invisible
+ // if app has called hideControlsOverlay() before view created.
+ mControlVisible = true;
+ if (!mControlVisibleBeforeOnCreateView) {
+ showControlsOverlay(false, false);
+ mControlVisibleBeforeOnCreateView = true;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (mControlVisible) {
+ //StateGraph: 6->5 1->2
+ if (mFadingEnabled) {
+ // StateGraph 1->2
+ startFadeTimer();
+ }
+ } else {
+ //StateGraph: 6->7 1->3
+ }
+ getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
+ getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
+ if (mHostCallback != null) {
+ mHostCallback.onHostResume();
+ }
+ }
+
+ private void stopFadeTimer() {
+ if (mHandler != null) {
+ mHandler.removeMessages(START_FADE_OUT);
+ }
+ }
+
+ private void startFadeTimer() {
+ if (mHandler != null) {
+ mHandler.removeMessages(START_FADE_OUT);
+ mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs);
+ }
+ }
+
+ private static ValueAnimator loadAnimator(Context context, int resId) {
+ ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId);
+ animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER);
+ return animator;
+ }
+
+ private void loadBgAnimator() {
+ AnimatorUpdateListener listener = new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator arg0) {
+ setBgAlpha((Integer) arg0.getAnimatedValue());
+ }
+ };
+
+ Context context = FragmentUtil.getContext(PlaybackFragment.this);
+ mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
+ mBgFadeInAnimator.addUpdateListener(listener);
+ mBgFadeInAnimator.addListener(mFadeListener);
+
+ mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
+ mBgFadeOutAnimator.addUpdateListener(listener);
+ mBgFadeOutAnimator.addListener(mFadeListener);
+ }
+
+ private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);
+ private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);
+
+ private void loadControlRowAnimator() {
+ final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator arg0) {
+ if (getVerticalGridView() == null) {
+ return;
+ }
+ RecyclerView.ViewHolder vh = getVerticalGridView()
+ .findViewHolderForAdapterPosition(0);
+ if (vh == null) {
+ return;
+ }
+ View view = vh.itemView;
+ if (view != null) {
+ final float fraction = (Float) arg0.getAnimatedValue();
+ if (DEBUG) Log.v(TAG, "fraction " + fraction);
+ view.setAlpha(fraction);
+ view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
+ }
+ }
+ };
+
+ Context context = FragmentUtil.getContext(PlaybackFragment.this);
+ mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
+ mControlRowFadeInAnimator.addUpdateListener(updateListener);
+ mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
+
+ mControlRowFadeOutAnimator = loadAnimator(context,
+ R.animator.lb_playback_controls_fade_out);
+ mControlRowFadeOutAnimator.addUpdateListener(updateListener);
+ mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
+ }
+
+ private void loadOtherRowAnimator() {
+ final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator arg0) {
+ if (getVerticalGridView() == null) {
+ return;
+ }
+ final float fraction = (Float) arg0.getAnimatedValue();
+ final int count = getVerticalGridView().getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = getVerticalGridView().getChildAt(i);
+ if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
+ view.setAlpha(fraction);
+ view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
+ }
+ }
+ }
+ };
+
+ Context context = FragmentUtil.getContext(PlaybackFragment.this);
+ mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
+ mOtherRowFadeInAnimator.addUpdateListener(updateListener);
+ mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
+
+ mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
+ mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
+ mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
+ }
+
+ /**
+ * Fades out the playback overlay immediately.
+ * @deprecated Call {@link #hideControlsOverlay(boolean)}
+ */
+ @Deprecated
+ public void fadeOut() {
+ showControlsOverlay(false, false);
+ }
+
+ /**
+ * Show controls overlay.
+ *
+ * @param runAnimation True to run animation, false otherwise.
+ */
+ public void showControlsOverlay(boolean runAnimation) {
+ showControlsOverlay(true, runAnimation);
+ }
+
+ /**
+ * Returns true if controls overlay is visible, false otherwise.
+ *
+ * @return True if controls overlay is visible, false otherwise.
+ * @see #showControlsOverlay(boolean)
+ * @see #hideControlsOverlay(boolean)
+ */
+ public boolean isControlsOverlayVisible() {
+ return mControlVisible;
+ }
+
+ /**
+ * Hide controls overlay.
+ *
+ * @param runAnimation True to run animation, false otherwise.
+ */
+ public void hideControlsOverlay(boolean runAnimation) {
+ showControlsOverlay(false, runAnimation);
+ }
+
+ /**
+ * if first animator is still running, reverse it; otherwise start second animator.
+ */
+ static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,
+ boolean runAnimation) {
+ if (first.isStarted()) {
+ first.reverse();
+ if (!runAnimation) {
+ first.end();
+ }
+ } else {
+ second.start();
+ if (!runAnimation) {
+ second.end();
+ }
+ }
+ }
+
+ /**
+ * End first or second animator if they are still running.
+ */
+ static void endAll(ValueAnimator first, ValueAnimator second) {
+ if (first.isStarted()) {
+ first.end();
+ } else if (second.isStarted()) {
+ second.end();
+ }
+ }
+
+ /**
+ * Fade in or fade out rows and background.
+ *
+ * @param show True to fade in, false to fade out.
+ * @param animation True to run animation.
+ */
+ void showControlsOverlay(boolean show, boolean animation) {
+ if (DEBUG) Log.v(TAG, "showControlsOverlay " + show);
+ if (getView() == null) {
+ mControlVisibleBeforeOnCreateView = show;
+ return;
+ }
+ // force no animation when fragment is not resumed
+ if (!isResumed()) {
+ animation = false;
+ }
+ if (show == mControlVisible) {
+ if (!animation) {
+ // End animation if needed
+ endAll(mBgFadeInAnimator, mBgFadeOutAnimator);
+ endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);
+ endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);
+ }
+ return;
+ }
+ // StateGraph: 7<->5 4<->3 2->3
+ mControlVisible = show;
+ if (!mControlVisible) {
+ // StateGraph 2->3
+ stopFadeTimer();
+ }
+
+ mAnimationTranslateY = (getVerticalGridView() == null
+ || getVerticalGridView().getSelectedPosition() == 0)
+ ? mMajorFadeTranslateY : mMinorFadeTranslateY;
+
+ if (show) {
+ reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);
+ reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,
+ animation);
+ reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);
+ } else {
+ reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);
+ reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,
+ animation);
+ reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);
+ }
+ if (animation) {
+ getView().announceForAccessibility(getString(show
+ ? R.string.lb_playback_controls_shown
+ : R.string.lb_playback_controls_hidden));
+ }
+ }
+
+ /**
+ * Sets the selected row position with smooth animation.
+ */
+ public void setSelectedPosition(int position) {
+ setSelectedPosition(position, true);
+ }
+
+ /**
+ * Sets the selected row position.
+ */
+ public void setSelectedPosition(int position, boolean smooth) {
+ mSetSelectionRunnable.mPosition = position;
+ mSetSelectionRunnable.mSmooth = smooth;
+ if (getView() != null && getView().getHandler() != null) {
+ getView().getHandler().post(mSetSelectionRunnable);
+ }
+ }
+
+ private void setupChildFragmentLayout() {
+ setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());
+ }
+
+ void setVerticalGridViewLayout(VerticalGridView listview) {
+ if (listview == null) {
+ return;
+ }
+
+ // we set the base line of alignment to -paddingBottom
+ listview.setWindowAlignmentOffset(-mPaddingBottom);
+ listview.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+
+ // align other rows that arent the last to center of screen, since our baseline is
+ // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom.
+ listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom);
+ listview.setItemAlignmentOffsetPercent(50);
+
+ // Push last row to the bottom padding
+ // Padding affects alignment when last row is focused
+ listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(),
+ listview.getPaddingRight(), mPaddingBottom);
+ listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mOtherRowsCenterToBottom = getResources()
+ .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom);
+ mPaddingBottom =
+ getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom);
+ mBgDarkColor =
+ getResources().getColor(R.color.lb_playback_controls_background_dark);
+ mBgLightColor =
+ getResources().getColor(R.color.lb_playback_controls_background_light);
+ mShowTimeMs =
+ getResources().getInteger(R.integer.lb_playback_controls_show_time_ms);
+ mMajorFadeTranslateY =
+ getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y);
+ mMinorFadeTranslateY =
+ getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y);
+
+ loadBgAnimator();
+ loadControlRowAnimator();
+ loadOtherRowAnimator();
+ }
+
+ /**
+ * Sets the background type.
+ *
+ * @param type One of BG_LIGHT, BG_DARK, or BG_NONE.
+ */
+ public void setBackgroundType(int type) {
+ switch (type) {
+ case BG_LIGHT:
+ case BG_DARK:
+ case BG_NONE:
+ if (type != mBackgroundType) {
+ mBackgroundType = type;
+ updateBackground();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid background type");
+ }
+ }
+
+ /**
+ * Returns the background type.
+ */
+ public int getBackgroundType() {
+ return mBackgroundType;
+ }
+
+ private void updateBackground() {
+ if (mBackgroundView != null) {
+ int color = mBgDarkColor;
+ switch (mBackgroundType) {
+ case BG_DARK:
+ break;
+ case BG_LIGHT:
+ color = mBgLightColor;
+ break;
+ case BG_NONE:
+ color = Color.TRANSPARENT;
+ break;
+ }
+ mBackgroundView.setBackground(new ColorDrawable(color));
+ setBgAlpha(mBgAlpha);
+ }
+ }
+
+ private final ItemBridgeAdapter.AdapterListener mAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
+ if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
+ if (!mControlVisible) {
+ if (DEBUG) Log.v(TAG, "setting alpha to 0");
+ vh.getViewHolder().view.setAlpha(0);
+ }
+ }
+
+ @Override
+ public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+ Presenter.ViewHolder viewHolder = vh.getViewHolder();
+ if (viewHolder instanceof PlaybackSeekUi) {
+ ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
+ if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
+ // Reset animation state
+ vh.getViewHolder().view.setAlpha(1f);
+ vh.getViewHolder().view.setTranslationY(0);
+ vh.getViewHolder().view.setAlpha(1f);
+ }
+
+ @Override
+ public void onBind(ItemBridgeAdapter.ViewHolder vh) {
+ }
+ };
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
+ mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
+ mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
+ R.id.playback_controls_dock);
+ if (mRowsFragment == null) {
+ mRowsFragment = new RowsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.playback_controls_dock, mRowsFragment)
+ .commit();
+ }
+ if (mAdapter == null) {
+ setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector()));
+ } else {
+ mRowsFragment.setAdapter(mAdapter);
+ }
+ mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
+
+ mBgAlpha = 255;
+ updateBackground();
+ mRowsFragment.setExternalAdapterListener(mAdapterListener);
+ ProgressBarManager progressBarManager = getProgressBarManager();
+ if (progressBarManager != null) {
+ progressBarManager.setRootView((ViewGroup) mRootView);
+ }
+ return mRootView;
+ }
+
+ /**
+ * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will
+ * take appropriate actions to take action when the hosting fragment starts/stops processing.
+ */
+ public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) {
+ this.mHostCallback = hostCallback;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setupChildFragmentLayout();
+ mRowsFragment.setAdapter(mAdapter);
+ if (mHostCallback != null) {
+ mHostCallback.onHostStart();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostStop();
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostPause();
+ }
+ if (mHandler.hasMessages(START_FADE_OUT)) {
+ // StateGraph: 2->1
+ mHandler.removeMessages(START_FADE_OUT);
+ } else {
+ // StateGraph: 5->6, 7->6, 4->1, 3->1
+ }
+ super.onPause();
+ }
+
+ /**
+ * This listener is called every time there is a selection in {@link RowsFragment}. This can
+ * be used by users to take additional actions such as animations.
+ */
+ public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
+ mExternalItemSelectedListener = listener;
+ }
+
+ /**
+ * This listener is called every time there is a click in {@link RowsFragment}. This can
+ * be used by users to take additional actions such as animations.
+ */
+ public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
+ mExternalItemClickedListener = listener;
+ }
+
+ /**
+ * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks
+ * only on {@link android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder}.
+ */
+ public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
+ mPlaybackItemClickedListener = listener;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mRootView = null;
+ mBackgroundView = null;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mHostCallback != null) {
+ mHostCallback.onHostDestroy();
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Sets the playback row for the playback controls. The row will be set as first element
+ * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}.
+ * @param row The row that represents the playback.
+ */
+ public void setPlaybackRow(Row row) {
+ this.mRow = row;
+ setupRow();
+ setupPresenter();
+ }
+
+ /**
+ * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If
+ * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will
+ * create a {@link ClassPresenterSelector} by default and map from the row object class to this
+ * {@link PlaybackRowPresenter}.
+ *
+ * @param presenter Presenter used to render {@link #setPlaybackRow(Row)}.
+ */
+ public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+ this.mPresenter = presenter;
+ setupPresenter();
+ setPlaybackRowPresenterAlignment();
+ }
+
+ void setPlaybackRowPresenterAlignment() {
+ if (mAdapter != null && mAdapter.getPresenterSelector() != null) {
+ Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters();
+ if (presenters != null) {
+ for (int i = 0; i < presenters.length; i++) {
+ if (presenters[i] instanceof PlaybackRowPresenter
+ && presenters[i].getFacet(ItemAlignmentFacet.class) == null) {
+ ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet();
+ ItemAlignmentFacet.ItemAlignmentDef def =
+ new ItemAlignmentFacet.ItemAlignmentDef();
+ def.setItemAlignmentOffset(0);
+ def.setItemAlignmentOffsetPercent(100);
+ itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]
+ {def});
+ presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the ui when the row data changes.
+ */
+ public void notifyPlaybackRowChanged() {
+ if (mAdapter == null) {
+ return;
+ }
+ mAdapter.notifyItemRangeChanged(0, 1);
+ }
+
+ /**
+ * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be
+ * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides
+ * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)},
+ * the row and presenter will be set onto the adapter.
+ *
+ * @param adapter The adapter that contains related rows and optional playback row.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ setupRow();
+ setupPresenter();
+ setPlaybackRowPresenterAlignment();
+
+ if (mRowsFragment != null) {
+ mRowsFragment.setAdapter(adapter);
+ }
+ }
+
+ private void setupRow() {
+ if (mAdapter instanceof ArrayObjectAdapter && mRow != null) {
+ ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter);
+ if (adapter.size() == 0) {
+ adapter.add(mRow);
+ } else {
+ adapter.replace(0, mRow);
+ }
+ } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) {
+ SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter);
+ adapter.set(0, mRow);
+ }
+ }
+
+ private void setupPresenter() {
+ if (mAdapter != null && mRow != null && mPresenter != null) {
+ PresenterSelector selector = mAdapter.getPresenterSelector();
+ if (selector == null) {
+ selector = new ClassPresenterSelector();
+ ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
+ mAdapter.setPresenterSelector(selector);
+ } else if (selector instanceof ClassPresenterSelector) {
+ ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
+ }
+ }
+ }
+
+ final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {
+ @Override
+ public boolean isSeekEnabled() {
+ return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();
+ }
+
+ @Override
+ public void onSeekStarted() {
+ if (mSeekUiClient != null) {
+ mSeekUiClient.onSeekStarted();
+ }
+ setSeekMode(true);
+ }
+
+ @Override
+ public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
+ return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();
+ }
+
+ @Override
+ public void onSeekPositionChanged(long pos) {
+ if (mSeekUiClient != null) {
+ mSeekUiClient.onSeekPositionChanged(pos);
+ }
+ }
+
+ @Override
+ public void onSeekFinished(boolean cancelled) {
+ if (mSeekUiClient != null) {
+ mSeekUiClient.onSeekFinished(cancelled);
+ }
+ setSeekMode(false);
+ }
+ };
+
+ /**
+ * Interface to be implemented by UI widget to support PlaybackSeekUi.
+ */
+ public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {
+ mSeekUiClient = client;
+ }
+
+ /**
+ * Show or hide other rows other than PlaybackRow.
+ * @param inSeek True to make other rows visible, false to make other rows invisible.
+ */
+ void setSeekMode(boolean inSeek) {
+ if (mInSeek == inSeek) {
+ return;
+ }
+ mInSeek = inSeek;
+ getVerticalGridView().setSelectedPosition(0);
+ if (mInSeek) {
+ stopFadeTimer();
+ }
+ // immediately fade in control row.
+ showControlsOverlay(true);
+ final int count = getVerticalGridView().getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = getVerticalGridView().getChildAt(i);
+ if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
+ view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);
+ }
+ }
+ }
+
+ /**
+ * Called when size of the video changes. App may override.
+ * @param videoWidth Intrinsic width of video
+ * @param videoHeight Intrinsic height of video
+ */
+ protected void onVideoSizeChanged(int videoWidth, int videoHeight) {
+ }
+
+ /**
+ * Called when media has start or stop buffering. App may override. The default initial state
+ * is not buffering.
+ * @param start True for buffering start, false otherwise.
+ */
+ protected void onBufferingStateChanged(boolean start) {
+ ProgressBarManager progressBarManager = getProgressBarManager();
+ if (progressBarManager != null) {
+ if (start) {
+ progressBarManager.show();
+ } else {
+ progressBarManager.hide();
+ }
+ }
+ }
+
+ /**
+ * Called when media has error. App may override.
+ * @param errorCode Optional error code for specific implementation.
+ * @param errorMessage Optional error message for specific implementation.
+ */
+ protected void onError(int errorCode, CharSequence errorMessage) {
+ }
+
+ /**
+ * Returns the ProgressBarManager that will show or hide progress bar in
+ * {@link #onBufferingStateChanged(boolean)}.
+ * @return The ProgressBarManager that will show or hide progress bar in
+ * {@link #onBufferingStateChanged(boolean)}.
+ */
+ public ProgressBarManager getProgressBarManager() {
+ return mProgressBarManager;
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
new file mode 100644
index 0000000..9e342fd
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
@@ -0,0 +1,142 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.PlaybackSeekUi;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.view.View;
+
+/**
+ * {@link PlaybackGlueHost} implementation
+ * the interaction between this class and {@link PlaybackFragment}.
+ * @deprecated use {@link PlaybackSupportFragmentGlueHost}
+ */
+@Deprecated
+public class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {
+ private final PlaybackFragment mFragment;
+
+ public PlaybackFragmentGlueHost(PlaybackFragment fragment) {
+ this.mFragment = fragment;
+ }
+
+ @Override
+ public void setControlsOverlayAutoHideEnabled(boolean enabled) {
+ mFragment.setControlsOverlayAutoHideEnabled(enabled);
+ }
+
+ @Override
+ public boolean isControlsOverlayAutoHideEnabled() {
+ return mFragment.isControlsOverlayAutoHideEnabled();
+ }
+
+ @Override
+ public void setOnKeyInterceptListener(View.OnKeyListener onKeyListener) {
+ mFragment.setOnKeyInterceptListener(onKeyListener);
+ }
+
+ @Override
+ public void setOnActionClickedListener(final OnActionClickedListener listener) {
+ if (listener == null) {
+ mFragment.setOnPlaybackItemViewClickedListener(null);
+ } else {
+ mFragment.setOnPlaybackItemViewClickedListener(new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ if (item instanceof Action) {
+ listener.onActionClicked((Action) item);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setHostCallback(HostCallback callback) {
+ mFragment.setHostCallback(callback);
+ }
+
+ @Override
+ public void notifyPlaybackRowChanged() {
+ mFragment.notifyPlaybackRowChanged();
+ }
+
+ @Override
+ public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+ mFragment.setPlaybackRowPresenter(presenter);
+ }
+
+ @Override
+ public void setPlaybackRow(Row row) {
+ mFragment.setPlaybackRow(row);
+ }
+
+ @Override
+ public void fadeOut() {
+ mFragment.fadeOut();
+ }
+
+ @Override
+ public boolean isControlsOverlayVisible() {
+ return mFragment.isControlsOverlayVisible();
+ }
+
+ @Override
+ public void hideControlsOverlay(boolean runAnimation) {
+ mFragment.hideControlsOverlay(runAnimation);
+ }
+
+ @Override
+ public void showControlsOverlay(boolean runAnimation) {
+ mFragment.showControlsOverlay(runAnimation);
+ }
+
+ @Override
+ public void setPlaybackSeekUiClient(Client client) {
+ mFragment.setPlaybackSeekUiClient(client);
+ }
+
+ final PlayerCallback mPlayerCallback =
+ new PlayerCallback() {
+ @Override
+ public void onBufferingStateChanged(boolean start) {
+ mFragment.onBufferingStateChanged(start);
+ }
+
+ @Override
+ public void onError(int errorCode, CharSequence errorMessage) {
+ mFragment.onError(errorCode, errorMessage);
+ }
+
+ @Override
+ public void onVideoSizeChanged(int videoWidth, int videoHeight) {
+ mFragment.onVideoSizeChanged(videoWidth, videoHeight);
+ }
+ };
+
+ @Override
+ public PlayerCallback getPlayerCallback() {
+ return mPlayerCallback;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java b/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
rename to leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
diff --git a/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/leanback/src/android/support/v17/leanback/app/RowsFragment.java
new file mode 100644
index 0000000..aa346bd
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -0,0 +1,689 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.animation.TimeAnimator;
+import android.animation.TimeAnimator.TimeListener;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.ViewHolderTask;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+
+/**
+ * An ordered set of rows of leanback widgets.
+ * <p>
+ * A RowsFragment renders the elements of its
+ * {@link android.support.v17.leanback.widget.ObjectAdapter} as a set
+ * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
+ * of {@link RowPresenter}.
+ * </p>
+ * @deprecated use {@link RowsSupportFragment}
+ */
+@Deprecated
+public class RowsFragment extends BaseRowFragment implements
+ BrowseFragment.MainFragmentRowsAdapterProvider,
+ BrowseFragment.MainFragmentAdapterProvider {
+
+ private MainFragmentAdapter mMainFragmentAdapter;
+ private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+
+ @Override
+ public BrowseFragment.MainFragmentAdapter getMainFragmentAdapter() {
+ if (mMainFragmentAdapter == null) {
+ mMainFragmentAdapter = new MainFragmentAdapter(this);
+ }
+ return mMainFragmentAdapter;
+ }
+
+ @Override
+ public BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter() {
+ if (mMainFragmentRowsAdapter == null) {
+ mMainFragmentRowsAdapter = new MainFragmentRowsAdapter(this);
+ }
+ return mMainFragmentRowsAdapter;
+ }
+
+ /**
+ * Internal helper class that manages row select animation and apply a default
+ * dim to each row.
+ */
+ final class RowViewHolderExtra implements TimeListener {
+ final RowPresenter mRowPresenter;
+ final Presenter.ViewHolder mRowViewHolder;
+
+ final TimeAnimator mSelectAnimator = new TimeAnimator();
+
+ int mSelectAnimatorDurationInUse;
+ Interpolator mSelectAnimatorInterpolatorInUse;
+ float mSelectLevelAnimStart;
+ float mSelectLevelAnimDelta;
+
+ RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) {
+ mRowPresenter = (RowPresenter) ibvh.getPresenter();
+ mRowViewHolder = ibvh.getViewHolder();
+ mSelectAnimator.setTimeListener(this);
+ }
+
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ if (mSelectAnimator.isRunning()) {
+ updateSelect(totalTime, deltaTime);
+ }
+ }
+
+ void updateSelect(long totalTime, long deltaTime) {
+ float fraction;
+ if (totalTime >= mSelectAnimatorDurationInUse) {
+ fraction = 1;
+ mSelectAnimator.end();
+ } else {
+ fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse);
+ }
+ if (mSelectAnimatorInterpolatorInUse != null) {
+ fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction);
+ }
+ float level = mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta;
+ mRowPresenter.setSelectLevel(mRowViewHolder, level);
+ }
+
+ void animateSelect(boolean select, boolean immediate) {
+ mSelectAnimator.end();
+ final float end = select ? 1 : 0;
+ if (immediate) {
+ mRowPresenter.setSelectLevel(mRowViewHolder, end);
+ } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) {
+ mSelectAnimatorDurationInUse = mSelectAnimatorDuration;
+ mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator;
+ mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder);
+ mSelectLevelAnimDelta = end - mSelectLevelAnimStart;
+ mSelectAnimator.start();
+ }
+ }
+
+ }
+
+ static final String TAG = "RowsFragment";
+ static final boolean DEBUG = false;
+ static final int ALIGN_TOP_NOT_SET = Integer.MIN_VALUE;
+
+ ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
+ private int mSubPosition;
+ boolean mExpand = true;
+ boolean mViewsCreated;
+ private int mAlignedTop = ALIGN_TOP_NOT_SET;
+ boolean mAfterEntranceTransition = true;
+ boolean mFreezeRows;
+
+ BaseOnItemViewSelectedListener mOnItemViewSelectedListener;
+ BaseOnItemViewClickedListener mOnItemViewClickedListener;
+
+ // Select animation and interpolator are not intended to be
+ // exposed at this moment. They might be synced with vertical scroll
+ // animation later.
+ int mSelectAnimatorDuration;
+ Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
+
+ private RecyclerView.RecycledViewPool mRecycledViewPool;
+ private ArrayList<Presenter> mPresenterMapper;
+
+ ItemBridgeAdapter.AdapterListener mExternalAdapterListener;
+
+ @Override
+ protected VerticalGridView findGridViewFromRoot(View view) {
+ return (VerticalGridView) view.findViewById(R.id.container_list);
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mViewsCreated) {
+ throw new IllegalStateException(
+ "Item clicked listener must be set before views are created");
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
+ * @deprecated use {@link BrowseFragment#enableRowScaling(boolean)} instead.
+ *
+ * @param enable true to enable row scaling
+ */
+ @Deprecated
+ public void enableRowScaling(boolean enable) {
+ }
+
+ /**
+ * Set the visibility of titles/hovercard of browse rows.
+ */
+ public void setExpand(boolean expand) {
+ mExpand = expand;
+ VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ final int count = listView.getChildCount();
+ if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count);
+ for (int i = 0; i < count; i++) {
+ View view = listView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder vh =
+ (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
+ setRowViewExpanded(vh, mExpand);
+ }
+ }
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ final int count = listView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = listView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
+ listView.getChildViewHolder(view);
+ getRowViewHolder(ibvh).setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ }
+ }
+ }
+
+ /**
+ * Returns an item selection listener.
+ */
+ public BaseOnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mOnItemViewSelectedListener;
+ }
+
+ @Override
+ void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,
+ int position, int subposition) {
+ if (mSelectedViewHolder != viewHolder || mSubPosition != subposition) {
+ if (DEBUG) Log.v(TAG, "new row selected position " + position + " subposition "
+ + subposition + " view " + viewHolder.itemView);
+ mSubPosition = subposition;
+ if (mSelectedViewHolder != null) {
+ setRowViewSelected(mSelectedViewHolder, false, false);
+ }
+ mSelectedViewHolder = (ItemBridgeAdapter.ViewHolder) viewHolder;
+ if (mSelectedViewHolder != null) {
+ setRowViewSelected(mSelectedViewHolder, true, false);
+ }
+ }
+ // When RowsFragment is embedded inside a page fragment, we want to show
+ // the title view only when we're on the first row or there is no data.
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.getFragmentHost().showTitleView(position <= 0);
+ }
+ }
+
+ /**
+ * Get row ViewHolder at adapter position. Returns null if the row object is not in adapter or
+ * the row object has not been bound to a row view.
+ *
+ * @param position Position of row in adapter.
+ * @return Row ViewHolder at a given adapter position.
+ */
+ public RowPresenter.ViewHolder getRowViewHolder(int position) {
+ VerticalGridView verticalView = getVerticalGridView();
+ if (verticalView == null) {
+ return null;
+ }
+ return getRowViewHolder((ItemBridgeAdapter.ViewHolder)
+ verticalView.findViewHolderForAdapterPosition(position));
+ }
+
+ @Override
+ int getLayoutResourceId() {
+ return R.layout.lb_rows_fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSelectAnimatorDuration = getResources().getInteger(
+ R.integer.lb_browse_rows_anim_duration);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ if (DEBUG) Log.v(TAG, "onViewCreated");
+ super.onViewCreated(view, savedInstanceState);
+ // Align the top edge of child with id row_content.
+ // Need set this for directly using RowsFragment.
+ getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
+ getVerticalGridView().setSaveChildrenPolicy(VerticalGridView.SAVE_LIMITED_CHILD);
+
+ setAlignment(mAlignedTop);
+
+ mRecycledViewPool = null;
+ mPresenterMapper = null;
+ if (mMainFragmentAdapter != null) {
+ mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
+ }
+
+ }
+
+ @Override
+ public void onDestroyView() {
+ mViewsCreated = false;
+ super.onDestroyView();
+ }
+
+ void setExternalAdapterListener(ItemBridgeAdapter.AdapterListener listener) {
+ mExternalAdapterListener = listener;
+ }
+
+ static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) {
+ ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded);
+ }
+
+ static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected,
+ boolean immediate) {
+ RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
+ extra.animateSelect(selected, immediate);
+ ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
+ }
+
+ private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onAddPresenter(Presenter presenter, int type) {
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onAddPresenter(presenter, type);
+ }
+ }
+
+ @Override
+ public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+ VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ // set clip children false for slide animation
+ listView.setClipChildren(false);
+ }
+ setupSharedViewPool(vh);
+ mViewsCreated = true;
+ vh.setExtraObject(new RowViewHolderExtra(vh));
+ // selected state is initialized to false, then driven by grid view onChildSelected
+ // events. When there is rebind, grid view fires onChildSelected event properly.
+ // So we don't need do anything special later in onBind or onAttachedToWindow.
+ setRowViewSelected(vh, false, true);
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onCreate(vh);
+ }
+ RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+ RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+ rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
+ if (DEBUG) Log.v(TAG, "onAttachToWindow");
+ // All views share the same mExpand value. When we attach a view to grid view,
+ // we should make sure it pick up the latest mExpand value we set early on other
+ // attached views. For no-structure-change update, the view is rebound to new data,
+ // but again it should use the unchanged mExpand value, so we don't need do any
+ // thing in onBind.
+ setRowViewExpanded(vh, mExpand);
+ RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+ RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+ rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
+
+ // freeze the rows attached after RowsFragment#freezeRows() is called
+ rowPresenter.freeze(rowVh, mFreezeRows);
+
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onAttachedToWindow(vh);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
+ if (mSelectedViewHolder == vh) {
+ setRowViewSelected(mSelectedViewHolder, false, true);
+ mSelectedViewHolder = null;
+ }
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onDetachedFromWindow(vh);
+ }
+ }
+
+ @Override
+ public void onBind(ItemBridgeAdapter.ViewHolder vh) {
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onBind(vh);
+ }
+ }
+
+ @Override
+ public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
+ setRowViewSelected(vh, false, true);
+ if (mExternalAdapterListener != null) {
+ mExternalAdapterListener.onUnbind(vh);
+ }
+ }
+ };
+
+ void setupSharedViewPool(ItemBridgeAdapter.ViewHolder bridgeVh) {
+ RowPresenter rowPresenter = (RowPresenter) bridgeVh.getPresenter();
+ RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(bridgeVh.getViewHolder());
+
+ if (rowVh instanceof ListRowPresenter.ViewHolder) {
+ HorizontalGridView view = ((ListRowPresenter.ViewHolder) rowVh).getGridView();
+ // Recycled view pool is shared between all list rows
+ if (mRecycledViewPool == null) {
+ mRecycledViewPool = view.getRecycledViewPool();
+ } else {
+ view.setRecycledViewPool(mRecycledViewPool);
+ }
+
+ ItemBridgeAdapter bridgeAdapter =
+ ((ListRowPresenter.ViewHolder) rowVh).getBridgeAdapter();
+ if (mPresenterMapper == null) {
+ mPresenterMapper = bridgeAdapter.getPresenterMapper();
+ } else {
+ bridgeAdapter.setPresenterMapper(mPresenterMapper);
+ }
+ }
+ }
+
+ @Override
+ void updateAdapter() {
+ super.updateAdapter();
+ mSelectedViewHolder = null;
+ mViewsCreated = false;
+
+ ItemBridgeAdapter adapter = getBridgeAdapter();
+ if (adapter != null) {
+ adapter.setAdapterListener(mBridgeAdapterListener);
+ }
+ }
+
+ @Override
+ public boolean onTransitionPrepare() {
+ boolean prepared = super.onTransitionPrepare();
+ if (prepared) {
+ freezeRows(true);
+ }
+ return prepared;
+ }
+
+ @Override
+ public void onTransitionEnd() {
+ super.onTransitionEnd();
+ freezeRows(false);
+ }
+
+ private void freezeRows(boolean freeze) {
+ mFreezeRows = freeze;
+ VerticalGridView verticalView = getVerticalGridView();
+ if (verticalView != null) {
+ final int count = verticalView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
+ verticalView.getChildViewHolder(verticalView.getChildAt(i));
+ RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
+ RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
+ rowPresenter.freeze(vh, freeze);
+ }
+ }
+ }
+
+ /**
+ * For rows that willing to participate entrance transition, this function
+ * hide views if afterTransition is true, show views if afterTransition is false.
+ */
+ public void setEntranceTransitionState(boolean afterTransition) {
+ mAfterEntranceTransition = afterTransition;
+ VerticalGridView verticalView = getVerticalGridView();
+ if (verticalView != null) {
+ final int count = verticalView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
+ verticalView.getChildViewHolder(verticalView.getChildAt(i));
+ RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
+ RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
+ rowPresenter.setEntranceTransitionState(vh, mAfterEntranceTransition);
+ }
+ }
+ }
+
+ /**
+ * Selects a Row and perform an optional task on the Row. For example
+ * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
+ * Scroll to 11th row and selects 6th item on that row. The method will be ignored if
+ * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
+ * ViewGroup, Bundle)}).
+ *
+ * @param rowPosition Which row to select.
+ * @param smooth True to scroll to the row, false for no animation.
+ * @param rowHolderTask Task to perform on the Row.
+ */
+ public void setSelectedPosition(int rowPosition, boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ VerticalGridView verticalView = getVerticalGridView();
+ if (verticalView == null) {
+ return;
+ }
+ ViewHolderTask task = null;
+ if (rowHolderTask != null) {
+ // This task will execute once the scroll completes. Once the scrolling finishes,
+ // we will get a success callback to update selected row position. Since the
+ // update to selected row position happens in a post, we want to ensure that this
+ // gets called after that.
+ task = new ViewHolderTask() {
+ @Override
+ public void run(final RecyclerView.ViewHolder rvh) {
+ rvh.itemView.post(new Runnable() {
+ @Override
+ public void run() {
+ rowHolderTask.run(
+ getRowViewHolder((ItemBridgeAdapter.ViewHolder) rvh));
+ }
+ });
+ }
+ };
+ }
+
+ if (smooth) {
+ verticalView.setSelectedPositionSmooth(rowPosition, task);
+ } else {
+ verticalView.setSelectedPosition(rowPosition, task);
+ }
+ }
+
+ static RowPresenter.ViewHolder getRowViewHolder(ItemBridgeAdapter.ViewHolder ibvh) {
+ if (ibvh == null) {
+ return null;
+ }
+ RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
+ return rowPresenter.getRowViewHolder(ibvh.getViewHolder());
+ }
+
+ public boolean isScrolling() {
+ if (getVerticalGridView() == null) {
+ return false;
+ }
+ return getVerticalGridView().getScrollState() != HorizontalGridView.SCROLL_STATE_IDLE;
+ }
+
+ @Override
+ public void setAlignment(int windowAlignOffsetFromTop) {
+ if (windowAlignOffsetFromTop == ALIGN_TOP_NOT_SET) {
+ return;
+ }
+ mAlignedTop = windowAlignOffsetFromTop;
+ final VerticalGridView gridView = getVerticalGridView();
+
+ if (gridView != null) {
+ gridView.setItemAlignmentOffset(0);
+ gridView.setItemAlignmentOffsetPercent(
+ VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ gridView.setItemAlignmentOffsetWithPadding(true);
+ gridView.setWindowAlignmentOffset(mAlignedTop);
+ // align to a fixed position from top
+ gridView.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+ }
+
+ /**
+ * Find row ViewHolder by position in adapter.
+ * @param position Position of row.
+ * @return ViewHolder of Row.
+ */
+ public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
+ if (mVerticalGridView == null) {
+ return null;
+ }
+ return getRowViewHolder((ItemBridgeAdapter.ViewHolder) mVerticalGridView
+ .findViewHolderForAdapterPosition(position));
+ }
+
+ public static class MainFragmentAdapter extends BrowseFragment.MainFragmentAdapter<RowsFragment> {
+
+ public MainFragmentAdapter(RowsFragment fragment) {
+ super(fragment);
+ setScalingEnabled(true);
+ }
+
+ @Override
+ public boolean isScrolling() {
+ return getFragment().isScrolling();
+ }
+
+ @Override
+ public void setExpand(boolean expand) {
+ getFragment().setExpand(expand);
+ }
+
+ @Override
+ public void setEntranceTransitionState(boolean state) {
+ getFragment().setEntranceTransitionState(state);
+ }
+
+ @Override
+ public void setAlignment(int windowAlignOffsetFromTop) {
+ getFragment().setAlignment(windowAlignOffsetFromTop);
+ }
+
+ @Override
+ public boolean onTransitionPrepare() {
+ return getFragment().onTransitionPrepare();
+ }
+
+ @Override
+ public void onTransitionStart() {
+ getFragment().onTransitionStart();
+ }
+
+ @Override
+ public void onTransitionEnd() {
+ getFragment().onTransitionEnd();
+ }
+
+ }
+
+ /**
+ * The adapter that RowsFragment implements
+ * BrowseFragment.MainFragmentRowsAdapter.
+ * @see #getMainFragmentRowsAdapter().
+ * @deprecated use {@link RowsSupportFragment}
+ */
+ @Deprecated
+ public static class MainFragmentRowsAdapter
+ extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> {
+
+ public MainFragmentRowsAdapter(RowsFragment fragment) {
+ super(fragment);
+ }
+
+ @Override
+ public void setAdapter(ObjectAdapter adapter) {
+ getFragment().setAdapter(adapter);
+ }
+
+ /**
+ * Sets an item clicked listener on the fragment.
+ */
+ @Override
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ getFragment().setOnItemViewClickedListener(listener);
+ }
+
+ @Override
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ getFragment().setOnItemViewSelectedListener(listener);
+ }
+
+ @Override
+ public void setSelectedPosition(int rowPosition,
+ boolean smooth,
+ final Presenter.ViewHolderTask rowHolderTask) {
+ getFragment().setSelectedPosition(rowPosition, smooth, rowHolderTask);
+ }
+
+ @Override
+ public void setSelectedPosition(int rowPosition, boolean smooth) {
+ getFragment().setSelectedPosition(rowPosition, smooth);
+ }
+
+ @Override
+ public int getSelectedPosition() {
+ return getFragment().getSelectedPosition();
+ }
+
+ @Override
+ public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
+ return getFragment().findRowViewHolderByPosition(position);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java b/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/leanback/src/android/support/v17/leanback/app/SearchFragment.java
new file mode 100644
index 0000000..00f2cca
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -0,0 +1,774 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SearchSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.Manifest;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.ObjectAdapter.DataObserver;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter.ViewHolder;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SearchBar;
+import android.support.v17.leanback.widget.SearchOrbView;
+import android.support.v17.leanback.widget.SpeechRecognitionCallback;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.CompletionInfo;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment to handle searches. An application will supply an implementation
+ * of the {@link SearchResultProvider} interface to handle the search and return
+ * an {@link ObjectAdapter} containing the results. The results are rendered
+ * into a {@link RowsFragment}, in the same way that they are in a {@link
+ * BrowseFragment}.
+ *
+ * <p>A SpeechRecognizer object will be created for which your application will need to declare
+ * android.permission.RECORD_AUDIO in AndroidManifest file. If app's target version is >= 23 and
+ * the device version is >= 23, a permission dialog will show first time using speech recognition.
+ * 0 will be used as requestCode in requestPermissions() call.
+ * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)} is deprecated.
+ * </p>
+ * <p>
+ * Speech recognition is automatically started when fragment is created, but
+ * not when fragment is restored from an instance state. Activity may manually
+ * call {@link #startRecognition()}, typically in onNewIntent().
+ * </p>
+ * @deprecated use {@link SearchSupportFragment}
+ */
+@Deprecated
+public class SearchFragment extends Fragment {
+ static final String TAG = SearchFragment.class.getSimpleName();
+ static final boolean DEBUG = false;
+
+ private static final String EXTRA_LEANBACK_BADGE_PRESENT = "LEANBACK_BADGE_PRESENT";
+ private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
+ private static final String ARG_QUERY = ARG_PREFIX + ".query";
+ private static final String ARG_TITLE = ARG_PREFIX + ".title";
+
+ static final long SPEECH_RECOGNITION_DELAY_MS = 300;
+
+ static final int RESULTS_CHANGED = 0x1;
+ static final int QUERY_COMPLETE = 0x2;
+
+ static final int AUDIO_PERMISSION_REQUEST_CODE = 0;
+
+ /**
+ * Search API to be provided by the application.
+ */
+ public static interface SearchResultProvider {
+ /**
+ * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve
+ * an ObjectAdapter that will contain the results to future updates of the search query.</p>
+ *
+ * <p>As results are retrieved, the application should use the data set notification methods
+ * on the ObjectAdapter to instruct the SearchFragment to update the results.</p>
+ *
+ * @return ObjectAdapter The result object adapter.
+ */
+ public ObjectAdapter getResultsAdapter();
+
+ /**
+ * <p>Method invoked when the search query is updated.</p>
+ *
+ * <p>This is called as soon as the query changes; it is up to the application to add a
+ * delay before actually executing the queries if needed.
+ *
+ * <p>This method might not always be called before onQueryTextSubmit gets called, in
+ * particular for voice input.
+ *
+ * @param newQuery The current search query.
+ * @return whether the results changed as a result of the new query.
+ */
+ public boolean onQueryTextChange(String newQuery);
+
+ /**
+ * Method invoked when the search query is submitted, either by dismissing the keyboard,
+ * pressing search or next on the keyboard or when voice has detected the end of the query.
+ *
+ * @param query The query entered.
+ * @return whether the results changed as a result of the query.
+ */
+ public boolean onQueryTextSubmit(String query);
+ }
+
+ final DataObserver mAdapterObserver = new DataObserver() {
+ @Override
+ public void onChanged() {
+ // onChanged() may be called multiple times e.g. the provider add
+ // rows to ArrayObjectAdapter one by one.
+ mHandler.removeCallbacks(mResultsChangedCallback);
+ mHandler.post(mResultsChangedCallback);
+ }
+ };
+
+ final Handler mHandler = new Handler();
+
+ final Runnable mResultsChangedCallback = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(TAG, "results changed, new size " + mResultAdapter.size());
+ if (mRowsFragment != null
+ && mRowsFragment.getAdapter() != mResultAdapter) {
+ if (!(mRowsFragment.getAdapter() == null && mResultAdapter.size() == 0)) {
+ mRowsFragment.setAdapter(mResultAdapter);
+ mRowsFragment.setSelectedPosition(0);
+ }
+ }
+ updateSearchBarVisibility();
+ mStatus |= RESULTS_CHANGED;
+ if ((mStatus & QUERY_COMPLETE) != 0) {
+ updateFocus();
+ }
+ updateSearchBarNextFocusId();
+ }
+ };
+
+ /**
+ * Runs when a new provider is set AND when the fragment view is created.
+ */
+ private final Runnable mSetSearchResultProvider = new Runnable() {
+ @Override
+ public void run() {
+ if (mRowsFragment == null) {
+ // We'll retry once we have a rows fragment
+ return;
+ }
+ // Retrieve the result adapter
+ ObjectAdapter adapter = mProvider.getResultsAdapter();
+ if (DEBUG) Log.v(TAG, "Got results adapter " + adapter);
+ if (adapter != mResultAdapter) {
+ boolean firstTime = mResultAdapter == null;
+ releaseAdapter();
+ mResultAdapter = adapter;
+ if (mResultAdapter != null) {
+ mResultAdapter.registerObserver(mAdapterObserver);
+ }
+ if (DEBUG) {
+ Log.v(TAG, "mResultAdapter " + mResultAdapter + " size "
+ + (mResultAdapter == null ? 0 : mResultAdapter.size()));
+ }
+ // delay the first time to avoid setting a empty result adapter
+ // until we got first onChange() from the provider
+ if (!(firstTime && (mResultAdapter == null || mResultAdapter.size() == 0))) {
+ mRowsFragment.setAdapter(mResultAdapter);
+ }
+ executePendingQuery();
+ }
+ updateSearchBarNextFocusId();
+
+ if (DEBUG) {
+ Log.v(TAG, "mAutoStartRecognition " + mAutoStartRecognition
+ + " mResultAdapter " + mResultAdapter
+ + " adapter " + mRowsFragment.getAdapter());
+ }
+ if (mAutoStartRecognition) {
+ mHandler.removeCallbacks(mStartRecognitionRunnable);
+ mHandler.postDelayed(mStartRecognitionRunnable, SPEECH_RECOGNITION_DELAY_MS);
+ } else {
+ updateFocus();
+ }
+ }
+ };
+
+ final Runnable mStartRecognitionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mAutoStartRecognition = false;
+ mSearchBar.startRecognition();
+ }
+ };
+
+ RowsFragment mRowsFragment;
+ SearchBar mSearchBar;
+ SearchResultProvider mProvider;
+ String mPendingQuery = null;
+
+ OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
+ ObjectAdapter mResultAdapter;
+ private SpeechRecognitionCallback mSpeechRecognitionCallback;
+
+ private String mTitle;
+ private Drawable mBadgeDrawable;
+ private ExternalQuery mExternalQuery;
+
+ private SpeechRecognizer mSpeechRecognizer;
+
+ int mStatus;
+ boolean mAutoStartRecognition = true;
+
+ private boolean mIsPaused;
+ private boolean mPendingStartRecognitionWhenPaused;
+ private SearchBar.SearchBarPermissionListener mPermissionListener =
+ new SearchBar.SearchBarPermissionListener() {
+ @Override
+ public void requestAudioPermission() {
+ PermissionHelper.requestPermissions(SearchFragment.this,
+ new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_PERMISSION_REQUEST_CODE);
+ }
+ };
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == AUDIO_PERMISSION_REQUEST_CODE && permissions.length > 0) {
+ if (permissions[0].equals(Manifest.permission.RECORD_AUDIO)
+ && grantResults[0] == PERMISSION_GRANTED) {
+ startRecognition();
+ }
+ }
+ }
+
+ /**
+ * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ */
+ public static Bundle createArgs(Bundle args, String query) {
+ return createArgs(args, query, null);
+ }
+
+ public static Bundle createArgs(Bundle args, String query, String title) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_QUERY, query);
+ args.putString(ARG_TITLE, title);
+ return args;
+ }
+
+ /**
+ * Creates a search fragment with a given search query.
+ *
+ * <p>You should only use this if you need to start the search fragment with a
+ * pre-filled query.
+ *
+ * @param query The search query to begin with.
+ * @return A new SearchFragment.
+ */
+ public static SearchFragment newInstance(String query) {
+ SearchFragment fragment = new SearchFragment();
+ Bundle args = createArgs(null, query);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (mAutoStartRecognition) {
+ mAutoStartRecognition = savedInstanceState == null;
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
+
+ FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame);
+ mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar);
+ mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
+ @Override
+ public void onSearchQueryChange(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s %s", query,
+ null == mProvider ? "(null)" : mProvider));
+ if (null != mProvider) {
+ retrieveResults(query);
+ } else {
+ mPendingQuery = query;
+ }
+ }
+
+ @Override
+ public void onSearchQuerySubmit(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query));
+ submitQuery(query);
+ }
+
+ @Override
+ public void onKeyboardDismiss(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query));
+ queryComplete();
+ }
+ });
+ mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
+ mSearchBar.setPermissionListener(mPermissionListener);
+ applyExternalQuery();
+
+ readArguments(getArguments());
+ if (null != mBadgeDrawable) {
+ setBadgeDrawable(mBadgeDrawable);
+ }
+ if (null != mTitle) {
+ setTitle(mTitle);
+ }
+
+ // Inject the RowsFragment in the results container
+ if (getChildFragmentManager().findFragmentById(R.id.lb_results_frame) == null) {
+ mRowsFragment = new RowsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.lb_results_frame, mRowsFragment).commit();
+ } else {
+ mRowsFragment = (RowsFragment) getChildFragmentManager()
+ .findFragmentById(R.id.lb_results_frame);
+ }
+ mRowsFragment.setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ if (DEBUG) {
+ int position = mRowsFragment.getSelectedPosition();
+ Log.v(TAG, String.format("onItemSelected %d", position));
+ }
+ updateSearchBarVisibility();
+ if (null != mOnItemViewSelectedListener) {
+ mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ });
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ mRowsFragment.setExpand(true);
+ if (null != mProvider) {
+ onSetSearchResultProvider();
+ }
+ return root;
+ }
+
+ private void resultsAvailable() {
+ if ((mStatus & QUERY_COMPLETE) != 0) {
+ focusOnResults();
+ }
+ updateSearchBarNextFocusId();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ VerticalGridView list = mRowsFragment.getVerticalGridView();
+ int mContainerListAlignTop =
+ getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top);
+ list.setItemAlignmentOffset(0);
+ list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ list.setWindowAlignmentOffset(mContainerListAlignTop);
+ list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ // VerticalGridView should not be focusable (see b/26894680 for details).
+ list.setFocusable(false);
+ list.setFocusableInTouchMode(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsPaused = false;
+ if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
+ mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
+ FragmentUtil.getContext(SearchFragment.this));
+ mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
+ }
+ if (mPendingStartRecognitionWhenPaused) {
+ mPendingStartRecognitionWhenPaused = false;
+ mSearchBar.startRecognition();
+ } else {
+ // Ensure search bar state consistency when using external recognizer
+ mSearchBar.stopRecognition();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ releaseRecognizer();
+ mIsPaused = true;
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ releaseAdapter();
+ super.onDestroy();
+ }
+
+ /**
+ * Returns RowsFragment that shows result rows. RowsFragment is initialized after
+ * SearchFragment.onCreateView().
+ *
+ * @return RowsFragment that shows result rows.
+ */
+ public RowsFragment getRowsFragment() {
+ return mRowsFragment;
+ }
+
+ private void releaseRecognizer() {
+ if (null != mSpeechRecognizer) {
+ mSearchBar.setSpeechRecognizer(null);
+ mSpeechRecognizer.destroy();
+ mSpeechRecognizer = null;
+ }
+ }
+
+ /**
+ * Starts speech recognition. Typical use case is that
+ * activity receives onNewIntent() call when user clicks a MIC button.
+ * Note that SearchFragment automatically starts speech recognition
+ * at first time created, there is no need to call startRecognition()
+ * when fragment is created.
+ */
+ public void startRecognition() {
+ if (mIsPaused) {
+ mPendingStartRecognitionWhenPaused = true;
+ } else {
+ mSearchBar.startRecognition();
+ }
+ }
+
+ /**
+ * Sets the search provider that is responsible for returning results for the
+ * search query.
+ */
+ public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
+ if (mProvider != searchResultProvider) {
+ mProvider = searchResultProvider;
+ onSetSearchResultProvider();
+ }
+ }
+
+ /**
+ * Sets an item selection listener for the results.
+ *
+ * @param listener The item selection listener to be invoked when an item in
+ * the search results is selected.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item clicked listener for the results.
+ *
+ * @param listener The item clicked listener to be invoked when an item in
+ * the search results is clicked.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ if (listener != mOnItemViewClickedListener) {
+ mOnItemViewClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ }
+ }
+
+ /**
+ * Sets the title string to be be shown in an empty search bar. The title
+ * may be placed in a call-to-action, such as "Search <i>title</i>" or
+ * "Speak to search <i>title</i>".
+ */
+ public void setTitle(String title) {
+ mTitle = title;
+ if (null != mSearchBar) {
+ mSearchBar.setTitle(title);
+ }
+ }
+
+ /**
+ * Returns the title set in the search bar.
+ */
+ public String getTitle() {
+ if (null != mSearchBar) {
+ return mSearchBar.getTitle();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the badge drawable that will be shown inside the search bar next to
+ * the title.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ mBadgeDrawable = drawable;
+ if (null != mSearchBar) {
+ mSearchBar.setBadgeDrawable(drawable);
+ }
+ }
+
+ /**
+ * Returns the badge drawable in the search bar.
+ */
+ public Drawable getBadgeDrawable() {
+ if (null != mSearchBar) {
+ return mSearchBar.getBadgeDrawable();
+ }
+ return null;
+ }
+
+ /**
+ * Sets background color of not-listening state search orb.
+ *
+ * @param colors SearchOrbView.Colors.
+ */
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ if (mSearchBar != null) {
+ mSearchBar.setSearchAffordanceColors(colors);
+ }
+ }
+
+ /**
+ * Sets background color of listening state search orb.
+ *
+ * @param colors SearchOrbView.Colors.
+ */
+ public void setSearchAffordanceColorsInListening(SearchOrbView.Colors colors) {
+ if (mSearchBar != null) {
+ mSearchBar.setSearchAffordanceColorsInListening(colors);
+ }
+ }
+
+ /**
+ * Displays the completions shown by the IME. An application may provide
+ * a list of query completions that the system will show in the IME.
+ *
+ * @param completions A list of completions to show in the IME. Setting to
+ * null or empty will clear the list.
+ */
+ public void displayCompletions(List<String> completions) {
+ mSearchBar.displayCompletions(completions);
+ }
+
+ /**
+ * Displays the completions shown by the IME. An application may provide
+ * a list of query completions that the system will show in the IME.
+ *
+ * @param completions A list of completions to show in the IME. Setting to
+ * null or empty will clear the list.
+ */
+ public void displayCompletions(CompletionInfo[] completions) {
+ mSearchBar.displayCompletions(completions);
+ }
+
+ /**
+ * Sets this callback to have the fragment pass speech recognition requests
+ * to the activity rather than using a SpeechRecognizer object.
+ * @deprecated Launching voice recognition activity is no longer supported. App should declare
+ * android.permission.RECORD_AUDIO in AndroidManifest file.
+ */
+ @Deprecated
+ public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) {
+ mSpeechRecognitionCallback = callback;
+ if (mSearchBar != null) {
+ mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
+ }
+ if (callback != null) {
+ releaseRecognizer();
+ }
+ }
+
+ /**
+ * Sets the text of the search query and optionally submits the query. Either
+ * {@link SearchResultProvider#onQueryTextChange onQueryTextChange} or
+ * {@link SearchResultProvider#onQueryTextSubmit onQueryTextSubmit} will be
+ * called on the provider if it is set.
+ *
+ * @param query The search query to set.
+ * @param submit Whether to submit the query.
+ */
+ public void setSearchQuery(String query, boolean submit) {
+ if (DEBUG) Log.v(TAG, "setSearchQuery " + query + " submit " + submit);
+ if (query == null) {
+ return;
+ }
+ mExternalQuery = new ExternalQuery(query, submit);
+ applyExternalQuery();
+ if (mAutoStartRecognition) {
+ mAutoStartRecognition = false;
+ mHandler.removeCallbacks(mStartRecognitionRunnable);
+ }
+ }
+
+ /**
+ * Sets the text of the search query based on the {@link RecognizerIntent#EXTRA_RESULTS} in
+ * the given intent, and optionally submit the query. If more than one result is present
+ * in the results list, the first will be used.
+ *
+ * @param intent Intent received from a speech recognition service.
+ * @param submit Whether to submit the query.
+ */
+ public void setSearchQuery(Intent intent, boolean submit) {
+ ArrayList<String> matches = intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
+ if (matches != null && matches.size() > 0) {
+ setSearchQuery(matches.get(0), submit);
+ }
+ }
+
+ /**
+ * Returns an intent that can be used to request speech recognition.
+ * Built from the base {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} plus
+ * extras:
+ *
+ * <ul>
+ * <li>{@link RecognizerIntent#EXTRA_LANGUAGE_MODEL} set to
+ * {@link RecognizerIntent#LANGUAGE_MODEL_FREE_FORM}</li>
+ * <li>{@link RecognizerIntent#EXTRA_PARTIAL_RESULTS} set to true</li>
+ * <li>{@link RecognizerIntent#EXTRA_PROMPT} set to the search bar hint text</li>
+ * </ul>
+ *
+ * For handling the intent returned from the service, see
+ * {@link #setSearchQuery(Intent, boolean)}.
+ */
+ public Intent getRecognizerIntent() {
+ Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
+ if (mSearchBar != null && mSearchBar.getHint() != null) {
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, mSearchBar.getHint());
+ }
+ recognizerIntent.putExtra(EXTRA_LEANBACK_BADGE_PRESENT, mBadgeDrawable != null);
+ return recognizerIntent;
+ }
+
+ void retrieveResults(String searchQuery) {
+ if (DEBUG) Log.v(TAG, "retrieveResults " + searchQuery);
+ if (mProvider.onQueryTextChange(searchQuery)) {
+ mStatus &= ~QUERY_COMPLETE;
+ }
+ }
+
+ void submitQuery(String query) {
+ queryComplete();
+ if (null != mProvider) {
+ mProvider.onQueryTextSubmit(query);
+ }
+ }
+
+ void queryComplete() {
+ if (DEBUG) Log.v(TAG, "queryComplete");
+ mStatus |= QUERY_COMPLETE;
+ focusOnResults();
+ }
+
+ void updateSearchBarVisibility() {
+ int position = mRowsFragment != null ? mRowsFragment.getSelectedPosition() : -1;
+ mSearchBar.setVisibility(position <=0 || mResultAdapter == null
+ || mResultAdapter.size() == 0 ? View.VISIBLE : View.GONE);
+ }
+
+ void updateSearchBarNextFocusId() {
+ if (mSearchBar == null || mResultAdapter == null) {
+ return;
+ }
+ final int viewId = (mResultAdapter.size() == 0 || mRowsFragment == null
+ || mRowsFragment.getVerticalGridView() == null)
+ ? 0 : mRowsFragment.getVerticalGridView().getId();
+ mSearchBar.setNextFocusDownId(viewId);
+ }
+
+ void updateFocus() {
+ if (mResultAdapter != null && mResultAdapter.size() > 0
+ && mRowsFragment != null && mRowsFragment.getAdapter() == mResultAdapter) {
+ focusOnResults();
+ } else {
+ mSearchBar.requestFocus();
+ }
+ }
+
+ private void focusOnResults() {
+ if (mRowsFragment == null || mRowsFragment.getVerticalGridView() == null
+ || mResultAdapter.size() == 0) {
+ return;
+ }
+ if (mRowsFragment.getVerticalGridView().requestFocus()) {
+ mStatus &= ~RESULTS_CHANGED;
+ }
+ }
+
+ private void onSetSearchResultProvider() {
+ mHandler.removeCallbacks(mSetSearchResultProvider);
+ mHandler.post(mSetSearchResultProvider);
+ }
+
+ void releaseAdapter() {
+ if (mResultAdapter != null) {
+ mResultAdapter.unregisterObserver(mAdapterObserver);
+ mResultAdapter = null;
+ }
+ }
+
+ void executePendingQuery() {
+ if (null != mPendingQuery && null != mResultAdapter) {
+ String query = mPendingQuery;
+ mPendingQuery = null;
+ retrieveResults(query);
+ }
+ }
+
+ private void applyExternalQuery() {
+ if (mExternalQuery == null || mSearchBar == null) {
+ return;
+ }
+ mSearchBar.setSearchQuery(mExternalQuery.mQuery);
+ if (mExternalQuery.mSubmit) {
+ submitQuery(mExternalQuery.mQuery);
+ }
+ mExternalQuery = null;
+ }
+
+ private void readArguments(Bundle args) {
+ if (null == args) {
+ return;
+ }
+ if (args.containsKey(ARG_QUERY)) {
+ setSearchQuery(args.getString(ARG_QUERY));
+ }
+
+ if (args.containsKey(ARG_TITLE)) {
+ setTitle(args.getString(ARG_TITLE));
+ }
+ }
+
+ private void setSearchQuery(String query) {
+ mSearchBar.setSearchQuery(query);
+ }
+
+ static class ExternalQuery {
+ String mQuery;
+ boolean mSubmit;
+
+ ExternalQuery(String query, boolean submit) {
+ mQuery = query;
+ mSubmit = submit;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java b/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
new file mode 100644
index 0000000..bff3dba
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -0,0 +1,260 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VerticalGridSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine.State;
+import android.support.v17.leanback.widget.BrowseFrameLayout;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnChildLaidOutListener;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.VerticalGridPresenter;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A fragment for creating leanback vertical grids.
+ *
+ * <p>Renders a vertical grid of objects given a {@link VerticalGridPresenter} and
+ * an {@link ObjectAdapter}.
+ * @deprecated use {@link VerticalGridSupportFragment}
+ */
+@Deprecated
+public class VerticalGridFragment extends BaseFragment {
+ static final String TAG = "VerticalGF";
+ static boolean DEBUG = false;
+
+ private ObjectAdapter mAdapter;
+ private VerticalGridPresenter mGridPresenter;
+ VerticalGridPresenter.ViewHolder mGridViewHolder;
+ OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
+ private Object mSceneAfterEntranceTransition;
+ private int mSelectedPosition = -1;
+
+ /**
+ * State to setEntranceTransitionState(false)
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionState(false);
+ }
+ };
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
+ }
+
+ /**
+ * Sets the grid presenter.
+ */
+ public void setGridPresenter(VerticalGridPresenter gridPresenter) {
+ if (gridPresenter == null) {
+ throw new IllegalArgumentException("Grid presenter may not be null");
+ }
+ mGridPresenter = gridPresenter;
+ mGridPresenter.setOnItemViewSelectedListener(mViewSelectedListener);
+ if (mOnItemViewClickedListener != null) {
+ mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ }
+
+ /**
+ * Returns the grid presenter.
+ */
+ public VerticalGridPresenter getGridPresenter() {
+ return mGridPresenter;
+ }
+
+ /**
+ * Sets the object adapter for the fragment.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ updateAdapter();
+ }
+
+ /**
+ * Returns the object adapter.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ final private OnItemViewSelectedListener mViewSelectedListener =
+ new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ int position = mGridViewHolder.getGridView().getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "grid selected position " + position);
+ gridOnItemSelected(position);
+ if (mOnItemViewSelectedListener != null) {
+ mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
+ final private OnChildLaidOutListener mChildLaidOutListener =
+ new OnChildLaidOutListener() {
+ @Override
+ public void onChildLaidOut(ViewGroup parent, View view, int position, long id) {
+ if (position == 0) {
+ showOrHideTitle();
+ }
+ }
+ };
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ void gridOnItemSelected(int position) {
+ if (position != mSelectedPosition) {
+ mSelectedPosition = position;
+ showOrHideTitle();
+ }
+ }
+
+ void showOrHideTitle() {
+ if (mGridViewHolder.getGridView().findViewHolderForAdapterPosition(mSelectedPosition)
+ == null) {
+ return;
+ }
+ if (!mGridViewHolder.getGridView().hasPreviousViewInSameRow(mSelectedPosition)) {
+ showTitle(true);
+ } else {
+ showTitle(false);
+ }
+ }
+
+ /**
+ * Sets an item clicked listener.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mGridPresenter != null) {
+ mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ ViewGroup root = (ViewGroup) inflater.inflate(R.layout.lb_vertical_grid_fragment,
+ container, false);
+ ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
+ installTitleView(inflater, gridFrame, savedInstanceState);
+ getProgressBarManager().setRootView(root);
+
+ ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
+ mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
+ gridDock.addView(mGridViewHolder.view);
+ mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
+
+ mSceneAfterEntranceTransition = TransitionHelper.createScene(gridDock, new Runnable() {
+ @Override
+ public void run() {
+ setEntranceTransitionState(true);
+ }
+ });
+
+ updateAdapter();
+ return root;
+ }
+
+ private void setupFocusSearchListener() {
+ BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(
+ R.id.grid_frame);
+ browseFrameLayout.setOnFocusSearchListener(getTitleHelper().getOnFocusSearchListener());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setupFocusSearchListener();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mGridViewHolder = null;
+ }
+
+ /**
+ * Sets the selected item position.
+ */
+ public void setSelectedPosition(int position) {
+ mSelectedPosition = position;
+ if(mGridViewHolder != null && mGridViewHolder.getGridView().getAdapter() != null) {
+ mGridViewHolder.getGridView().setSelectedPositionSmooth(position);
+ }
+ }
+
+ private void updateAdapter() {
+ if (mGridViewHolder != null) {
+ mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter);
+ if (mSelectedPosition != -1) {
+ mGridViewHolder.getGridView().setSelectedPosition(mSelectedPosition);
+ }
+ }
+ }
+
+ @Override
+ protected Object createEntranceTransition() {
+ return TransitionHelper.loadTransition(FragmentUtil.getContext(VerticalGridFragment.this),
+ R.transition.lb_vertical_grid_entrance_transition);
+ }
+
+ @Override
+ protected void runEntranceTransition(Object entranceTransition) {
+ TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
+ }
+
+ void setEntranceTransitionState(boolean afterTransition) {
+ mGridPresenter.setEntranceTransitionState(mGridViewHolder, afterTransition);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java b/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
diff --git a/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/leanback/src/android/support/v17/leanback/app/VideoFragment.java
new file mode 100644
index 0000000..e4d75f3
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -0,0 +1,122 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.R;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView}
+ * and rendering video.
+ * @deprecated use {@link VideoSupportFragment}
+ */
+@Deprecated
+public class VideoFragment extends PlaybackFragment {
+ static final int SURFACE_NOT_CREATED = 0;
+ static final int SURFACE_CREATED = 1;
+
+ SurfaceView mVideoSurface;
+ SurfaceHolder.Callback mMediaPlaybackCallback;
+
+ int mState = SURFACE_NOT_CREATED;
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
+ mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(VideoFragment.this)).inflate(
+ R.layout.lb_video_surface, root, false);
+ root.addView(mVideoSurface, 0);
+ mVideoSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ if (mMediaPlaybackCallback != null) {
+ mMediaPlaybackCallback.surfaceCreated(holder);
+ }
+ mState = SURFACE_CREATED;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (mMediaPlaybackCallback != null) {
+ mMediaPlaybackCallback.surfaceChanged(holder, format, width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (mMediaPlaybackCallback != null) {
+ mMediaPlaybackCallback.surfaceDestroyed(holder);
+ }
+ mState = SURFACE_NOT_CREATED;
+ }
+ });
+ setBackgroundType(PlaybackFragment.BG_LIGHT);
+ return root;
+ }
+
+ /**
+ * Adds {@link SurfaceHolder.Callback} to {@link android.view.SurfaceView}.
+ */
+ public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
+ mMediaPlaybackCallback = callback;
+
+ if (callback != null) {
+ if (mState == SURFACE_CREATED) {
+ mMediaPlaybackCallback.surfaceCreated(mVideoSurface.getHolder());
+ }
+ }
+ }
+
+ @Override
+ protected void onVideoSizeChanged(int width, int height) {
+ int screenWidth = getView().getWidth();
+ int screenHeight = getView().getHeight();
+
+ ViewGroup.LayoutParams p = mVideoSurface.getLayoutParams();
+ if (screenWidth * height > width * screenHeight) {
+ // fit in screen height
+ p.height = screenHeight;
+ p.width = screenHeight * width / height;
+ } else {
+ // fit in screen width
+ p.width = screenWidth;
+ p.height = screenWidth * height / width;
+ }
+ mVideoSurface.setLayoutParams(p);
+ }
+
+ /**
+ * Returns the surface view.
+ */
+ public SurfaceView getSurfaceView() {
+ return mVideoSurface;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mVideoSurface = null;
+ mState = SURFACE_NOT_CREATED;
+ super.onDestroyView();
+ }
+}
diff --git a/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
new file mode 100644
index 0000000..546e581
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
@@ -0,0 +1,49 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoSupportFragmentGlueHost.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.media.SurfaceHolderGlueHost;
+import android.view.SurfaceHolder;
+
+/**
+ * {@link PlaybackGlueHost} implementation
+ * the interaction between {@link PlaybackGlue} and {@link VideoFragment}.
+ * @deprecated use {@link VideoSupportFragmentGlueHost}
+ */
+@Deprecated
+public class VideoFragmentGlueHost extends PlaybackFragmentGlueHost
+ implements SurfaceHolderGlueHost {
+ private final VideoFragment mFragment;
+
+ public VideoFragmentGlueHost(VideoFragment fragment) {
+ super(fragment);
+ this.mFragment = fragment;
+ }
+
+ /**
+ * Sets the {@link android.view.SurfaceHolder.Callback} on the host.
+ * {@link PlaybackGlueHost} is assumed to either host the {@link SurfaceHolder} or
+ * have a reference to the component hosting it for rendering the video.
+ */
+ @Override
+ public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
+ mFragment.setSurfaceHolderCallback(callback);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java b/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/package-info.java b/leanback/src/android/support/v17/leanback/app/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/package-info.java
rename to leanback/src/android/support/v17/leanback/app/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java b/leanback/src/android/support/v17/leanback/database/CursorMapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java
rename to leanback/src/android/support/v17/leanback/database/CursorMapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java b/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
rename to leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java b/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java b/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java b/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java b/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
rename to leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java b/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
rename to leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java b/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java b/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
rename to leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java b/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java b/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
rename to leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java b/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java b/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java b/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
rename to leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/package-info.java b/leanback/src/android/support/v17/leanback/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/package-info.java
rename to leanback/src/android/support/v17/leanback/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/system/Settings.java b/leanback/src/android/support/v17/leanback/system/Settings.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/system/Settings.java
rename to leanback/src/android/support/v17/leanback/system/Settings.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java b/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
rename to leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java b/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
rename to leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java b/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
rename to leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/util/MathUtil.java b/leanback/src/android/support/v17/leanback/util/MathUtil.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/util/MathUtil.java
rename to leanback/src/android/support/v17/leanback/util/MathUtil.java
diff --git a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java b/leanback/src/android/support/v17/leanback/util/StateMachine.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
rename to leanback/src/android/support/v17/leanback/util/StateMachine.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Action.java b/leanback/src/android/support/v17/leanback/widget/Action.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Action.java
rename to leanback/src/android/support/v17/leanback/widget/Action.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
diff --git a/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
new file mode 100644
index 0000000..2dcf51f
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.support.annotation.Nullable;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.util.ListUpdateCallback;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An {@link ObjectAdapter} implemented with an {@link ArrayList}.
+ */
+public class ArrayObjectAdapter extends ObjectAdapter {
+
+ private static final Boolean DEBUG = false;
+ private static final String TAG = "ArrayObjectAdapter";
+
+ private final List mItems = new ArrayList<Object>();
+
+ // To compute the payload correctly, we should use a temporary list to hold all the old items.
+ private final List mOldItems = new ArrayList<Object>();
+
+ // Un modifiable version of mItems;
+ private List mUnmodifiableItems;
+
+ /**
+ * Constructs an adapter with the given {@link PresenterSelector}.
+ */
+ public ArrayObjectAdapter(PresenterSelector presenterSelector) {
+ super(presenterSelector);
+ }
+
+ /**
+ * Constructs an adapter that uses the given {@link Presenter} for all items.
+ */
+ public ArrayObjectAdapter(Presenter presenter) {
+ super(presenter);
+ }
+
+ /**
+ * Constructs an adapter.
+ */
+ public ArrayObjectAdapter() {
+ super();
+ }
+
+ @Override
+ public int size() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object get(int index) {
+ return mItems.get(index);
+ }
+
+ /**
+ * Returns the index for the first occurrence of item in the adapter, or -1 if
+ * not found.
+ *
+ * @param item The item to find in the list.
+ * @return Index of the first occurrence of the item in the adapter, or -1
+ * if not found.
+ */
+ public int indexOf(Object item) {
+ return mItems.indexOf(item);
+ }
+
+ /**
+ * Notify that the content of a range of items changed. Note that this is
+ * not same as items being added or removed.
+ *
+ * @param positionStart The position of first item that has changed.
+ * @param itemCount The count of how many items have changed.
+ */
+ public void notifyArrayItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Adds an item to the end of the adapter.
+ *
+ * @param item The item to add to the end of the adapter.
+ */
+ public void add(Object item) {
+ add(mItems.size(), item);
+ }
+
+ /**
+ * Inserts an item into this adapter at the specified index.
+ * If the index is > {@link #size} an exception will be thrown.
+ *
+ * @param index The index at which the item should be inserted.
+ * @param item The item to insert into the adapter.
+ */
+ public void add(int index, Object item) {
+ mItems.add(index, item);
+ notifyItemRangeInserted(index, 1);
+ }
+
+ /**
+ * Adds the objects in the given collection to the adapter, starting at the
+ * given index. If the index is >= {@link #size} an exception will be thrown.
+ *
+ * @param index The index at which the items should be inserted.
+ * @param items A {@link Collection} of items to insert.
+ */
+ public void addAll(int index, Collection items) {
+ int itemsCount = items.size();
+ if (itemsCount == 0) {
+ return;
+ }
+ mItems.addAll(index, items);
+ notifyItemRangeInserted(index, itemsCount);
+ }
+
+ /**
+ * Removes the first occurrence of the given item from the adapter.
+ *
+ * @param item The item to remove from the adapter.
+ * @return True if the item was found and thus removed from the adapter.
+ */
+ public boolean remove(Object item) {
+ int index = mItems.indexOf(item);
+ if (index >= 0) {
+ mItems.remove(index);
+ notifyItemRangeRemoved(index, 1);
+ }
+ return index >= 0;
+ }
+
+ /**
+ * Moved the item at fromPosition to toPosition.
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ public void move(int fromPosition, int toPosition) {
+ if (fromPosition == toPosition) {
+ // no-op
+ return;
+ }
+ Object item = mItems.remove(fromPosition);
+ mItems.add(toPosition, item);
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /**
+ * Replaces item at position with a new item and calls notifyItemRangeChanged()
+ * at the given position. Note that this method does not compare new item to
+ * existing item.
+ *
+ * @param position The index of item to replace.
+ * @param item The new item to be placed at given position.
+ */
+ public void replace(int position, Object item) {
+ mItems.set(position, item);
+ notifyItemRangeChanged(position, 1);
+ }
+
+ /**
+ * Removes a range of items from the adapter. The range is specified by giving
+ * the starting position and the number of elements to remove.
+ *
+ * @param position The index of the first item to remove.
+ * @param count The number of items to remove.
+ * @return The number of items removed.
+ */
+ public int removeItems(int position, int count) {
+ int itemsToRemove = Math.min(count, mItems.size() - position);
+ if (itemsToRemove <= 0) {
+ return 0;
+ }
+
+ for (int i = 0; i < itemsToRemove; i++) {
+ mItems.remove(position);
+ }
+ notifyItemRangeRemoved(position, itemsToRemove);
+ return itemsToRemove;
+ }
+
+ /**
+ * Removes all items from this adapter, leaving it empty.
+ */
+ public void clear() {
+ int itemCount = mItems.size();
+ if (itemCount == 0) {
+ return;
+ }
+ mItems.clear();
+ notifyItemRangeRemoved(0, itemCount);
+ }
+
+ /**
+ * Gets a read-only view of the list of object of this ArrayObjectAdapter.
+ */
+ public <E> List<E> unmodifiableList() {
+
+ // The mUnmodifiableItems will only be created once as long as the content of mItems has not
+ // been changed.
+ if (mUnmodifiableItems == null) {
+ mUnmodifiableItems = Collections.unmodifiableList(mItems);
+ }
+ return mUnmodifiableItems;
+ }
+
+ @Override
+ public boolean isImmediateNotifySupported() {
+ return true;
+ }
+
+ ListUpdateCallback mListUpdateCallback;
+
+ /**
+ * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to
+ * specified position.
+ *
+ * @param itemList List of new Items
+ * @param callback Optional DiffCallback Object to compute the difference between the old data
+ * set and new data set. When null, {@link #notifyChanged()} will be fired.
+ */
+ public void setItems(final List itemList, final DiffCallback callback) {
+ if (callback == null) {
+ // shortcut when DiffCallback is not provided
+ mItems.clear();
+ mItems.addAll(itemList);
+ notifyChanged();
+ return;
+ }
+ mOldItems.clear();
+ mOldItems.addAll(mItems);
+
+ DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
+ @Override
+ public int getOldListSize() {
+ return mOldItems.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return itemList.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return callback.areItemsTheSame(mOldItems.get(oldItemPosition),
+ itemList.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return callback.areContentsTheSame(mOldItems.get(oldItemPosition),
+ itemList.get(newItemPosition));
+ }
+
+ @Nullable
+ @Override
+ public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ return callback.getChangePayload(mOldItems.get(oldItemPosition),
+ itemList.get(newItemPosition));
+ }
+ });
+
+ // update items.
+ mItems.clear();
+ mItems.addAll(itemList);
+
+ // dispatch diff result
+ if (mListUpdateCallback == null) {
+ mListUpdateCallback = new ListUpdateCallback() {
+
+ @Override
+ public void onInserted(int position, int count) {
+ if (DEBUG) {
+ Log.d(TAG, "onInserted");
+ }
+ notifyItemRangeInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ if (DEBUG) {
+ Log.d(TAG, "onRemoved");
+ }
+ notifyItemRangeRemoved(position, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ if (DEBUG) {
+ Log.d(TAG, "onMoved");
+ }
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ if (DEBUG) {
+ Log.d(TAG, "onChanged");
+ }
+ notifyItemRangeChanged(position, count, payload);
+ }
+ };
+ }
+ diffResult.dispatchUpdatesTo(mListUpdateCallback);
+ mOldItems.clear();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java b/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
rename to leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java b/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
rename to leanback/src/android/support/v17/leanback/widget/BaseCardView.java
diff --git a/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
new file mode 100644
index 0000000..2ebec47
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -0,0 +1,1202 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SimpleItemAnimator;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * An abstract base class for vertically and horizontally scrolling lists. The items come
+ * from the {@link RecyclerView.Adapter} associated with this view.
+ * Do not directly use this class, use {@link VerticalGridView} and {@link HorizontalGridView}.
+ * The class is not intended to be subclassed other than {@link VerticalGridView} and
+ * {@link HorizontalGridView}.
+ */
+public abstract class BaseGridView extends RecyclerView {
+
+ /**
+ * Always keep focused item at a aligned position. Developer can use
+ * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
+ * In this mode, the last focused position will be remembered and restored when focus
+ * is back to the view.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final static int FOCUS_SCROLL_ALIGNED = 0;
+
+ /**
+ * Scroll to make the focused item inside client area.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final static int FOCUS_SCROLL_ITEM = 1;
+
+ /**
+ * Scroll a page of items when focusing to item outside the client area.
+ * The page size matches the client area size of RecyclerView.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final static int FOCUS_SCROLL_PAGE = 2;
+
+ /**
+ * The first item is aligned with the low edge of the viewport. When
+ * navigating away from the first item, the focus item is aligned to a key line location.
+ * <p>
+ * For HorizontalGridView, low edge refers to getPaddingLeft() when RTL is false or
+ * getWidth() - getPaddingRight() when RTL is true.
+ * For VerticalGridView, low edge refers to getPaddingTop().
+ * <p>
+ * The key line location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ * <p>
+ * Note if there are very few items between low edge and key line, use
+ * {@link #setWindowAlignmentPreferKeyLineOverLowEdge(boolean)} to control whether you prefer
+ * to align the items to key line or low edge. Default is preferring low edge.
+ */
+ public final static int WINDOW_ALIGN_LOW_EDGE = 1;
+
+ /**
+ * The last item is aligned with the high edge of the viewport when
+ * navigating to the end of list. When navigating away from the end, the
+ * focus item is aligned to a key line location.
+ * <p>
+ * For HorizontalGridView, high edge refers to getWidth() - getPaddingRight() when RTL is false
+ * or getPaddingLeft() when RTL is true.
+ * For VerticalGridView, high edge refers to getHeight() - getPaddingBottom().
+ * <p>
+ * The key line location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ * <p>
+ * Note if there are very few items between high edge and key line, use
+ * {@link #setWindowAlignmentPreferKeyLineOverHighEdge(boolean)} to control whether you prefer
+ * to align the items to key line or high edge. Default is preferring key line.
+ */
+ public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
+
+ /**
+ * The first item and last item are aligned with the two edges of the
+ * viewport. When navigating in the middle of list, the focus maintains a
+ * key line location.
+ * <p>
+ * The key line location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_BOTH_EDGE =
+ WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
+
+ /**
+ * The focused item always stays in a key line location.
+ * <p>
+ * The key line location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_NO_EDGE = 0;
+
+ /**
+ * Value indicates that percent is not used.
+ */
+ public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1;
+
+ /**
+ * Value indicates that percent is not used.
+ */
+ public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED =
+ ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED;
+
+ /**
+ * Dont save states of any child views.
+ */
+ public static final int SAVE_NO_CHILD = 0;
+
+ /**
+ * Only save on screen child views, the states are lost when they become off screen.
+ */
+ public static final int SAVE_ON_SCREEN_CHILD = 1;
+
+ /**
+ * Save on screen views plus save off screen child views states up to
+ * {@link #getSaveChildrenLimitNumber()}.
+ */
+ public static final int SAVE_LIMITED_CHILD = 2;
+
+ /**
+ * Save on screen views plus save off screen child views without any limitation.
+ * This might cause out of memory, only use it when you are dealing with limited data.
+ */
+ public static final int SAVE_ALL_CHILD = 3;
+
+ /**
+ * Listener for intercepting touch dispatch events.
+ */
+ public interface OnTouchInterceptListener {
+ /**
+ * Returns true if the touch dispatch event should be consumed.
+ */
+ public boolean onInterceptTouchEvent(MotionEvent event);
+ }
+
+ /**
+ * Listener for intercepting generic motion dispatch events.
+ */
+ public interface OnMotionInterceptListener {
+ /**
+ * Returns true if the touch dispatch event should be consumed.
+ */
+ public boolean onInterceptMotionEvent(MotionEvent event);
+ }
+
+ /**
+ * Listener for intercepting key dispatch events.
+ */
+ public interface OnKeyInterceptListener {
+ /**
+ * Returns true if the key dispatch event should be consumed.
+ */
+ public boolean onInterceptKeyEvent(KeyEvent event);
+ }
+
+ public interface OnUnhandledKeyListener {
+ /**
+ * Returns true if the key event should be consumed.
+ */
+ public boolean onUnhandledKey(KeyEvent event);
+ }
+
+ final GridLayoutManager mLayoutManager;
+
+ /**
+ * Animate layout changes from a child resizing or adding/removing a child.
+ */
+ private boolean mAnimateChildLayout = true;
+
+ private boolean mHasOverlappingRendering = true;
+
+ private RecyclerView.ItemAnimator mSavedItemAnimator;
+
+ private OnTouchInterceptListener mOnTouchInterceptListener;
+ private OnMotionInterceptListener mOnMotionInterceptListener;
+ private OnKeyInterceptListener mOnKeyInterceptListener;
+ RecyclerView.RecyclerListener mChainedRecyclerListener;
+ private OnUnhandledKeyListener mOnUnhandledKeyListener;
+
+ /**
+ * Number of items to prefetch when first coming on screen with new data.
+ */
+ int mInitialPrefetchItemCount = 4;
+
+ BaseGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mLayoutManager = new GridLayoutManager(this);
+ setLayoutManager(mLayoutManager);
+ // leanback LayoutManager already restores focus inside onLayoutChildren().
+ setPreserveFocusAfterLayout(false);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setHasFixedSize(true);
+ setChildrenDrawingOrderEnabled(true);
+ setWillNotDraw(true);
+ setOverScrollMode(View.OVER_SCROLL_NEVER);
+ // Disable change animation by default on leanback.
+ // Change animation will create a new view and cause undesired
+ // focus animation between the old view and new view.
+ ((SimpleItemAnimator)getItemAnimator()).setSupportsChangeAnimations(false);
+ super.setRecyclerListener(new RecyclerView.RecyclerListener() {
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ mLayoutManager.onChildRecycled(holder);
+ if (mChainedRecyclerListener != null) {
+ mChainedRecyclerListener.onViewRecycled(holder);
+ }
+ }
+ });
+ }
+
+ void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
+ boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
+ boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
+ mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
+ boolean throughSideStart = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideStart, true);
+ boolean throughSideEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideEnd, true);
+ mLayoutManager.setFocusOutSideAllowed(throughSideStart, throughSideEnd);
+ mLayoutManager.setVerticalSpacing(
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_android_verticalSpacing,
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0)));
+ mLayoutManager.setHorizontalSpacing(
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_android_horizontalSpacing,
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0)));
+ if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
+ setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
+ }
+ a.recycle();
+ }
+
+ /**
+ * Sets the strategy used to scroll in response to item focus changing:
+ * <ul>
+ * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
+ * <li>{@link #FOCUS_SCROLL_ITEM}</li>
+ * <li>{@link #FOCUS_SCROLL_PAGE}</li>
+ * </ul>
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setFocusScrollStrategy(int scrollStrategy) {
+ if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
+ && scrollStrategy != FOCUS_SCROLL_PAGE) {
+ throw new IllegalArgumentException("Invalid scrollStrategy");
+ }
+ mLayoutManager.setFocusScrollStrategy(scrollStrategy);
+ requestLayout();
+ }
+
+ /**
+ * Returns the strategy used to scroll in response to item focus changing.
+ * <ul>
+ * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
+ * <li>{@link #FOCUS_SCROLL_ITEM}</li>
+ * <li>{@link #FOCUS_SCROLL_PAGE}</li>
+ * </ul>
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public int getFocusScrollStrategy() {
+ return mLayoutManager.getFocusScrollStrategy();
+ }
+
+ /**
+ * Sets the method for focused item alignment in the view.
+ *
+ * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
+ * {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
+ * {@link #WINDOW_ALIGN_NO_EDGE}.
+ */
+ public void setWindowAlignment(int windowAlignment) {
+ mLayoutManager.setWindowAlignment(windowAlignment);
+ requestLayout();
+ }
+
+ /**
+ * Returns the method for focused item alignment in the view.
+ *
+ * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
+ * {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
+ */
+ public int getWindowAlignment() {
+ return mLayoutManager.getWindowAlignment();
+ }
+
+ /**
+ * Sets whether prefer key line over low edge when {@link #WINDOW_ALIGN_LOW_EDGE} is used.
+ * When true, if there are very few items between low edge and key line, align items to key
+ * line instead of align items to low edge.
+ * Default value is false (aka prefer align to low edge).
+ *
+ * @param preferKeyLineOverLowEdge True to prefer key line over low edge, false otherwise.
+ */
+ public void setWindowAlignmentPreferKeyLineOverLowEdge(boolean preferKeyLineOverLowEdge) {
+ mLayoutManager.mWindowAlignment.mainAxis()
+ .setPreferKeylineOverLowEdge(preferKeyLineOverLowEdge);
+ requestLayout();
+ }
+
+
+ /**
+ * Returns whether prefer key line over high edge when {@link #WINDOW_ALIGN_HIGH_EDGE} is used.
+ * When true, if there are very few items between high edge and key line, align items to key
+ * line instead of align items to high edge.
+ * Default value is true (aka prefer align to key line).
+ *
+ * @param preferKeyLineOverHighEdge True to prefer key line over high edge, false otherwise.
+ */
+ public void setWindowAlignmentPreferKeyLineOverHighEdge(boolean preferKeyLineOverHighEdge) {
+ mLayoutManager.mWindowAlignment.mainAxis()
+ .setPreferKeylineOverHighEdge(preferKeyLineOverHighEdge);
+ requestLayout();
+ }
+
+ /**
+ * Returns whether prefer key line over low edge when {@link #WINDOW_ALIGN_LOW_EDGE} is used.
+ * When true, if there are very few items between low edge and key line, align items to key
+ * line instead of align items to low edge.
+ * Default value is false (aka prefer align to low edge).
+ *
+ * @return True to prefer key line over low edge, false otherwise.
+ */
+ public boolean isWindowAlignmentPreferKeyLineOverLowEdge() {
+ return mLayoutManager.mWindowAlignment.mainAxis().isPreferKeylineOverLowEdge();
+ }
+
+
+ /**
+ * Returns whether prefer key line over high edge when {@link #WINDOW_ALIGN_HIGH_EDGE} is used.
+ * When true, if there are very few items between high edge and key line, align items to key
+ * line instead of align items to high edge.
+ * Default value is true (aka prefer align to key line).
+ *
+ * @return True to prefer key line over high edge, false otherwise.
+ */
+ public boolean isWindowAlignmentPreferKeyLineOverHighEdge() {
+ return mLayoutManager.mWindowAlignment.mainAxis().isPreferKeylineOverHighEdge();
+ }
+
+
+ /**
+ * Sets the offset in pixels for window alignment key line.
+ *
+ * @param offset The number of pixels to offset. If the offset is positive,
+ * it is distance from low edge (see {@link #WINDOW_ALIGN_LOW_EDGE});
+ * if the offset is negative, the absolute value is distance from high
+ * edge (see {@link #WINDOW_ALIGN_HIGH_EDGE}).
+ * Default value is 0.
+ */
+ public void setWindowAlignmentOffset(int offset) {
+ mLayoutManager.setWindowAlignmentOffset(offset);
+ requestLayout();
+ }
+
+ /**
+ * Returns the offset in pixels for window alignment key line.
+ *
+ * @return The number of pixels to offset. If the offset is positive,
+ * it is distance from low edge (see {@link #WINDOW_ALIGN_LOW_EDGE});
+ * if the offset is negative, the absolute value is distance from high
+ * edge (see {@link #WINDOW_ALIGN_HIGH_EDGE}).
+ * Default value is 0.
+ */
+ public int getWindowAlignmentOffset() {
+ return mLayoutManager.getWindowAlignmentOffset();
+ }
+
+ /**
+ * Sets the offset percent for window alignment key line in addition to {@link
+ * #getWindowAlignmentOffset()}.
+ *
+ * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
+ * width from low edge. Use
+ * {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
+ * Default value is 50.
+ */
+ public void setWindowAlignmentOffsetPercent(float offsetPercent) {
+ mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
+ requestLayout();
+ }
+
+ /**
+ * Returns the offset percent for window alignment key line in addition to
+ * {@link #getWindowAlignmentOffset()}.
+ *
+ * @return Percentage to offset. E.g., 40 means 40% of the width from the
+ * low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
+ * disabled. Default value is 50.
+ */
+ public float getWindowAlignmentOffsetPercent() {
+ return mLayoutManager.getWindowAlignmentOffsetPercent();
+ }
+
+ /**
+ * Sets number of pixels to the end of low edge. Supports right to left layout direction.
+ * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
+ * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
+ *
+ * @param offset In left to right or vertical case, it's the offset added to left/top edge.
+ * In right to left case, it's the offset subtracted from right edge.
+ */
+ public void setItemAlignmentOffset(int offset) {
+ mLayoutManager.setItemAlignmentOffset(offset);
+ requestLayout();
+ }
+
+ /**
+ * Returns number of pixels to the end of low edge. Supports right to left layout direction. In
+ * left to right or vertical case, it's the offset added to left/top edge. In right to left
+ * case, it's the offset subtracted from right edge.
+ * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
+ * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
+ *
+ * @return The number of pixels to the end of low edge.
+ */
+ public int getItemAlignmentOffset() {
+ return mLayoutManager.getItemAlignmentOffset();
+ }
+
+ /**
+ * Sets whether applies padding to item alignment when {@link #getItemAlignmentOffsetPercent()}
+ * is 0 or 100.
+ * <p>When true:
+ * Applies start/top padding if {@link #getItemAlignmentOffsetPercent()} is 0.
+ * Applies end/bottom padding if {@link #getItemAlignmentOffsetPercent()} is 100.
+ * Does not apply padding if {@link #getItemAlignmentOffsetPercent()} is neither 0 nor 100.
+ * </p>
+ * <p>When false: does not apply padding</p>
+ */
+ public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
+ mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
+ requestLayout();
+ }
+
+ /**
+ * Returns true if applies padding to item alignment when
+ * {@link #getItemAlignmentOffsetPercent()} is 0 or 100; returns false otherwise.
+ * <p>When true:
+ * Applies start/top padding when {@link #getItemAlignmentOffsetPercent()} is 0.
+ * Applies end/bottom padding when {@link #getItemAlignmentOffsetPercent()} is 100.
+ * Does not apply padding if {@link #getItemAlignmentOffsetPercent()} is neither 0 nor 100.
+ * </p>
+ * <p>When false: does not apply padding</p>
+ */
+ public boolean isItemAlignmentOffsetWithPadding() {
+ return mLayoutManager.isItemAlignmentOffsetWithPadding();
+ }
+
+ /**
+ * Sets the offset percent for item alignment in addition to {@link
+ * #getItemAlignmentOffset()}.
+ * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
+ * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
+ *
+ * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
+ * width from the low edge. Use
+ * {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
+ */
+ public void setItemAlignmentOffsetPercent(float offsetPercent) {
+ mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
+ requestLayout();
+ }
+
+ /**
+ * Returns the offset percent for item alignment in addition to {@link
+ * #getItemAlignmentOffset()}.
+ *
+ * @return Percentage to offset. E.g., 40 means 40% of the width from the
+ * low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
+ * disabled. Default value is 50.
+ */
+ public float getItemAlignmentOffsetPercent() {
+ return mLayoutManager.getItemAlignmentOffsetPercent();
+ }
+
+ /**
+ * Sets the id of the view to align with. Use {@link android.view.View#NO_ID} (default)
+ * for the root {@link RecyclerView.ViewHolder#itemView}.
+ * Item alignment settings on BaseGridView are if {@link ItemAlignmentFacet}
+ * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
+ */
+ public void setItemAlignmentViewId(int viewId) {
+ mLayoutManager.setItemAlignmentViewId(viewId);
+ }
+
+ /**
+ * Returns the id of the view to align with, or {@link android.view.View#NO_ID} for the root
+ * {@link RecyclerView.ViewHolder#itemView}.
+ * @return The id of the view to align with, or {@link android.view.View#NO_ID} for the root
+ * {@link RecyclerView.ViewHolder#itemView}.
+ */
+ public int getItemAlignmentViewId() {
+ return mLayoutManager.getItemAlignmentViewId();
+ }
+
+ /**
+ * Sets the spacing in pixels between two child items.
+ * @deprecated use {@link #setItemSpacing(int)}
+ */
+ @Deprecated
+ public void setItemMargin(int margin) {
+ setItemSpacing(margin);
+ }
+
+ /**
+ * Sets the vertical and horizontal spacing in pixels between two child items.
+ * @param spacing Vertical and horizontal spacing in pixels between two child items.
+ */
+ public void setItemSpacing(int spacing) {
+ mLayoutManager.setItemSpacing(spacing);
+ requestLayout();
+ }
+
+ /**
+ * Sets the spacing in pixels between two child items vertically.
+ * @deprecated Use {@link #setVerticalSpacing(int)}
+ */
+ @Deprecated
+ public void setVerticalMargin(int margin) {
+ setVerticalSpacing(margin);
+ }
+
+ /**
+ * Returns the spacing in pixels between two child items vertically.
+ * @deprecated Use {@link #getVerticalSpacing()}
+ */
+ @Deprecated
+ public int getVerticalMargin() {
+ return mLayoutManager.getVerticalSpacing();
+ }
+
+ /**
+ * Sets the spacing in pixels between two child items horizontally.
+ * @deprecated Use {@link #setHorizontalSpacing(int)}
+ */
+ @Deprecated
+ public void setHorizontalMargin(int margin) {
+ setHorizontalSpacing(margin);
+ }
+
+ /**
+ * Returns the spacing in pixels between two child items horizontally.
+ * @deprecated Use {@link #getHorizontalSpacing()}
+ */
+ @Deprecated
+ public int getHorizontalMargin() {
+ return mLayoutManager.getHorizontalSpacing();
+ }
+
+ /**
+ * Sets the vertical spacing in pixels between two child items.
+ * @param spacing Vertical spacing between two child items.
+ */
+ public void setVerticalSpacing(int spacing) {
+ mLayoutManager.setVerticalSpacing(spacing);
+ requestLayout();
+ }
+
+ /**
+ * Returns the vertical spacing in pixels between two child items.
+ * @return The vertical spacing in pixels between two child items.
+ */
+ public int getVerticalSpacing() {
+ return mLayoutManager.getVerticalSpacing();
+ }
+
+ /**
+ * Sets the horizontal spacing in pixels between two child items.
+ * @param spacing Horizontal spacing in pixels between two child items.
+ */
+ public void setHorizontalSpacing(int spacing) {
+ mLayoutManager.setHorizontalSpacing(spacing);
+ requestLayout();
+ }
+
+ /**
+ * Returns the horizontal spacing in pixels between two child items.
+ * @return The Horizontal spacing in pixels between two child items.
+ */
+ public int getHorizontalSpacing() {
+ return mLayoutManager.getHorizontalSpacing();
+ }
+
+ /**
+ * Registers a callback to be invoked when an item in BaseGridView has
+ * been laid out.
+ *
+ * @param listener The listener to be invoked.
+ */
+ public void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
+ mLayoutManager.setOnChildLaidOutListener(listener);
+ }
+
+ /**
+ * Registers a callback to be invoked when an item in BaseGridView has
+ * been selected. Note that the listener may be invoked when there is a
+ * layout pending on the view, affording the listener an opportunity to
+ * adjust the upcoming layout based on the selection state.
+ *
+ * @param listener The listener to be invoked.
+ */
+ public void setOnChildSelectedListener(OnChildSelectedListener listener) {
+ mLayoutManager.setOnChildSelectedListener(listener);
+ }
+
+ /**
+ * Registers a callback to be invoked when an item in BaseGridView has
+ * been selected. Note that the listener may be invoked when there is a
+ * layout pending on the view, affording the listener an opportunity to
+ * adjust the upcoming layout based on the selection state.
+ * This method will clear all existing listeners added by
+ * {@link #addOnChildViewHolderSelectedListener}.
+ *
+ * @param listener The listener to be invoked.
+ */
+ public void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
+ mLayoutManager.setOnChildViewHolderSelectedListener(listener);
+ }
+
+ /**
+ * Registers a callback to be invoked when an item in BaseGridView has
+ * been selected. Note that the listener may be invoked when there is a
+ * layout pending on the view, affording the listener an opportunity to
+ * adjust the upcoming layout based on the selection state.
+ *
+ * @param listener The listener to be invoked.
+ */
+ public void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
+ mLayoutManager.addOnChildViewHolderSelectedListener(listener);
+ }
+
+ /**
+ * Remove the callback invoked when an item in BaseGridView has been selected.
+ *
+ * @param listener The listener to be removed.
+ */
+ public void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener)
+ {
+ mLayoutManager.removeOnChildViewHolderSelectedListener(listener);
+ }
+
+ /**
+ * Changes the selected item immediately without animation.
+ */
+ public void setSelectedPosition(int position) {
+ mLayoutManager.setSelection(position, 0);
+ }
+
+ /**
+ * Changes the selected item and/or subposition immediately without animation.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setSelectedPositionWithSub(int position, int subposition) {
+ mLayoutManager.setSelectionWithSub(position, subposition, 0);
+ }
+
+ /**
+ * Changes the selected item immediately without animation, scrollExtra is
+ * applied in primary scroll direction. The scrollExtra will be kept until
+ * another {@link #setSelectedPosition} or {@link #setSelectedPositionSmooth} call.
+ */
+ public void setSelectedPosition(int position, int scrollExtra) {
+ mLayoutManager.setSelection(position, scrollExtra);
+ }
+
+ /**
+ * Changes the selected item and/or subposition immediately without animation, scrollExtra is
+ * applied in primary scroll direction. The scrollExtra will be kept until
+ * another {@link #setSelectedPosition} or {@link #setSelectedPositionSmooth} call.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setSelectedPositionWithSub(int position, int subposition, int scrollExtra) {
+ mLayoutManager.setSelectionWithSub(position, subposition, scrollExtra);
+ }
+
+ /**
+ * Changes the selected item and run an animation to scroll to the target
+ * position.
+ * @param position Adapter position of the item to select.
+ */
+ public void setSelectedPositionSmooth(int position) {
+ mLayoutManager.setSelectionSmooth(position);
+ }
+
+ /**
+ * Changes the selected item and/or subposition, runs an animation to scroll to the target
+ * position.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setSelectedPositionSmoothWithSub(int position, int subposition) {
+ mLayoutManager.setSelectionSmoothWithSub(position, subposition);
+ }
+
+ /**
+ * Perform a task on ViewHolder at given position after smooth scrolling to it.
+ * @param position Position of item in adapter.
+ * @param task Task to executed on the ViewHolder at a given position.
+ */
+ public void setSelectedPositionSmooth(final int position, final ViewHolderTask task) {
+ if (task != null) {
+ RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
+ if (vh == null || hasPendingAdapterUpdates()) {
+ addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
+ @Override
+ public void onChildViewHolderSelected(RecyclerView parent,
+ RecyclerView.ViewHolder child, int selectedPosition, int subposition) {
+ if (selectedPosition == position) {
+ removeOnChildViewHolderSelectedListener(this);
+ task.run(child);
+ }
+ }
+ });
+ } else {
+ task.run(vh);
+ }
+ }
+ setSelectedPositionSmooth(position);
+ }
+
+ /**
+ * Perform a task on ViewHolder at given position after scroll to it.
+ * @param position Position of item in adapter.
+ * @param task Task to executed on the ViewHolder at a given position.
+ */
+ public void setSelectedPosition(final int position, final ViewHolderTask task) {
+ if (task != null) {
+ RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
+ if (vh == null || hasPendingAdapterUpdates()) {
+ addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
+ @Override
+ public void onChildViewHolderSelectedAndPositioned(RecyclerView parent,
+ RecyclerView.ViewHolder child, int selectedPosition, int subposition) {
+ if (selectedPosition == position) {
+ removeOnChildViewHolderSelectedListener(this);
+ task.run(child);
+ }
+ }
+ });
+ } else {
+ task.run(vh);
+ }
+ }
+ setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the adapter position of selected item.
+ * @return The adapter position of selected item.
+ */
+ public int getSelectedPosition() {
+ return mLayoutManager.getSelection();
+ }
+
+ /**
+ * Returns the sub selected item position started from zero. An item can have
+ * multiple {@link ItemAlignmentFacet}s provided by {@link RecyclerView.ViewHolder}
+ * or {@link FacetProviderAdapter}. Zero is returned when no {@link ItemAlignmentFacet}
+ * is defined.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public int getSelectedSubPosition() {
+ return mLayoutManager.getSubSelection();
+ }
+
+ /**
+ * Sets whether ItemAnimator should run when a child changes size or when adding
+ * or removing a child.
+ * @param animateChildLayout True to enable ItemAnimator, false to disable.
+ */
+ public void setAnimateChildLayout(boolean animateChildLayout) {
+ if (mAnimateChildLayout != animateChildLayout) {
+ mAnimateChildLayout = animateChildLayout;
+ if (!mAnimateChildLayout) {
+ mSavedItemAnimator = getItemAnimator();
+ super.setItemAnimator(null);
+ } else {
+ super.setItemAnimator(mSavedItemAnimator);
+ }
+ }
+ }
+
+ /**
+ * Returns true if an animation will run when a child changes size or when
+ * adding or removing a child.
+ * @return True if ItemAnimator is enabled, false otherwise.
+ */
+ public boolean isChildLayoutAnimated() {
+ return mAnimateChildLayout;
+ }
+
+ /**
+ * Sets the gravity used for child view positioning. Defaults to
+ * GRAVITY_TOP|GRAVITY_START.
+ *
+ * @param gravity See {@link android.view.Gravity}
+ */
+ public void setGravity(int gravity) {
+ mLayoutManager.setGravity(gravity);
+ requestLayout();
+ }
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
+ previouslyFocusedRect);
+ }
+
+ /**
+ * Returns the x/y offsets to final position from current position if the view
+ * is selected.
+ *
+ * @param view The view to get offsets.
+ * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of Y.
+ */
+ public void getViewSelectedOffsets(View view, int[] offsets) {
+ mLayoutManager.getViewSelectedOffsets(view, offsets);
+ }
+
+ @Override
+ public int getChildDrawingOrder(int childCount, int i) {
+ return mLayoutManager.getChildDrawingOrder(this, childCount, i);
+ }
+
+ final boolean isChildrenDrawingOrderEnabledInternal() {
+ return isChildrenDrawingOrderEnabled();
+ }
+
+ @Override
+ public View focusSearch(int direction) {
+ if (isFocused()) {
+ // focusSearch(int) is called when GridView itself is focused.
+ // Calling focusSearch(view, int) to get next sibling of current selected child.
+ View view = mLayoutManager.findViewByPosition(mLayoutManager.getSelection());
+ if (view != null) {
+ return focusSearch(view, direction);
+ }
+ }
+ // otherwise, go to mParent to perform focusSearch
+ return super.focusSearch(direction);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ mLayoutManager.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ }
+
+ /**
+ * Disables or enables focus search.
+ * @param disabled True to disable focus search, false to enable.
+ */
+ public final void setFocusSearchDisabled(boolean disabled) {
+ // LayoutManager may detachView and attachView in fastRelayout, it causes RowsFragment
+ // re-gain focus after a BACK key pressed, so block children focus during transition.
+ setDescendantFocusability(disabled ? FOCUS_BLOCK_DESCENDANTS: FOCUS_AFTER_DESCENDANTS);
+ mLayoutManager.setFocusSearchDisabled(disabled);
+ }
+
+ /**
+ * Returns true if focus search is disabled.
+ * @return True if focus search is disabled.
+ */
+ public final boolean isFocusSearchDisabled() {
+ return mLayoutManager.isFocusSearchDisabled();
+ }
+
+ /**
+ * Enables or disables layout. All children will be removed when layout is
+ * disabled.
+ * @param layoutEnabled True to enable layout, false otherwise.
+ */
+ public void setLayoutEnabled(boolean layoutEnabled) {
+ mLayoutManager.setLayoutEnabled(layoutEnabled);
+ }
+
+ /**
+ * Changes and overrides children's visibility.
+ * @param visibility See {@link View#getVisibility()}.
+ */
+ public void setChildrenVisibility(int visibility) {
+ mLayoutManager.setChildrenVisibility(visibility);
+ }
+
+ /**
+ * Enables or disables pruning of children. Disable is useful during transition.
+ * @param pruneChild True to prune children out side visible area, false to enable.
+ */
+ public void setPruneChild(boolean pruneChild) {
+ mLayoutManager.setPruneChild(pruneChild);
+ }
+
+ /**
+ * Enables or disables scrolling. Disable is useful during transition.
+ * @param scrollEnabled True to enable scroll, false to disable.
+ */
+ public void setScrollEnabled(boolean scrollEnabled) {
+ mLayoutManager.setScrollEnabled(scrollEnabled);
+ }
+
+ /**
+ * Returns true if scrolling is enabled, false otherwise.
+ * @return True if scrolling is enabled, false otherwise.
+ */
+ public boolean isScrollEnabled() {
+ return mLayoutManager.isScrollEnabled();
+ }
+
+ /**
+ * Returns true if the view at the given position has a same row sibling
+ * in front of it. This will return true if first item view is not created.
+ *
+ * @param position Position in adapter.
+ * @return True if the view at the given position has a same row sibling in front of it.
+ */
+ public boolean hasPreviousViewInSameRow(int position) {
+ return mLayoutManager.hasPreviousViewInSameRow(position);
+ }
+
+ /**
+ * Enables or disables the default "focus draw at last" order rule. Default is enabled.
+ * @param enabled True to draw the selected child at last, false otherwise.
+ */
+ public void setFocusDrawingOrderEnabled(boolean enabled) {
+ super.setChildrenDrawingOrderEnabled(enabled);
+ }
+
+ /**
+ * Returns true if draws selected child at last, false otherwise. Default is enabled.
+ * @return True if draws selected child at last, false otherwise.
+ */
+ public boolean isFocusDrawingOrderEnabled() {
+ return super.isChildrenDrawingOrderEnabled();
+ }
+
+ /**
+ * Sets the touch intercept listener.
+ * @param listener The touch intercept listener.
+ */
+ public void setOnTouchInterceptListener(OnTouchInterceptListener listener) {
+ mOnTouchInterceptListener = listener;
+ }
+
+ /**
+ * Sets the generic motion intercept listener.
+ * @param listener The motion intercept listener.
+ */
+ public void setOnMotionInterceptListener(OnMotionInterceptListener listener) {
+ mOnMotionInterceptListener = listener;
+ }
+
+ /**
+ * Sets the key intercept listener.
+ * @param listener The key intercept listener.
+ */
+ public void setOnKeyInterceptListener(OnKeyInterceptListener listener) {
+ mOnKeyInterceptListener = listener;
+ }
+
+ /**
+ * Sets the unhandled key listener.
+ * @param listener The unhandled key intercept listener.
+ */
+ public void setOnUnhandledKeyListener(OnUnhandledKeyListener listener) {
+ mOnUnhandledKeyListener = listener;
+ }
+
+ /**
+ * Returns the unhandled key listener.
+ * @return The unhandled key listener.
+ */
+ public OnUnhandledKeyListener getOnUnhandledKeyListener() {
+ return mOnUnhandledKeyListener;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mOnKeyInterceptListener != null && mOnKeyInterceptListener.onInterceptKeyEvent(event)) {
+ return true;
+ }
+ if (super.dispatchKeyEvent(event)) {
+ return true;
+ }
+ return mOnUnhandledKeyListener != null && mOnUnhandledKeyListener.onUnhandledKey(event);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (mOnTouchInterceptListener != null) {
+ if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) {
+ return true;
+ }
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
+ if (mOnMotionInterceptListener != null) {
+ if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) {
+ return true;
+ }
+ }
+ return super.dispatchGenericFocusedEvent(event);
+ }
+
+ /**
+ * Returns the policy for saving children.
+ *
+ * @return policy, one of {@link #SAVE_NO_CHILD}
+ * {@link #SAVE_ON_SCREEN_CHILD} {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
+ */
+ public final int getSaveChildrenPolicy() {
+ return mLayoutManager.mChildrenStates.getSavePolicy();
+ }
+
+ /**
+ * Returns the limit used when when {@link #getSaveChildrenPolicy()} is
+ * {@link #SAVE_LIMITED_CHILD}
+ */
+ public final int getSaveChildrenLimitNumber() {
+ return mLayoutManager.mChildrenStates.getLimitNumber();
+ }
+
+ /**
+ * Sets the policy for saving children.
+ * @param savePolicy One of {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
+ * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
+ */
+ public final void setSaveChildrenPolicy(int savePolicy) {
+ mLayoutManager.mChildrenStates.setSavePolicy(savePolicy);
+ }
+
+ /**
+ * Sets the limit number when {@link #getSaveChildrenPolicy()} is {@link #SAVE_LIMITED_CHILD}.
+ */
+ public final void setSaveChildrenLimitNumber(int limitNumber) {
+ mLayoutManager.mChildrenStates.setLimitNumber(limitNumber);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return mHasOverlappingRendering;
+ }
+
+ public void setHasOverlappingRendering(boolean hasOverlapping) {
+ mHasOverlappingRendering = hasOverlapping;
+ }
+
+ /**
+ * Notify layout manager that layout directionality has been updated
+ */
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ mLayoutManager.onRtlPropertiesChanged(layoutDirection);
+ }
+
+ @Override
+ public void setRecyclerListener(RecyclerView.RecyclerListener listener) {
+ mChainedRecyclerListener = listener;
+ }
+
+ /**
+ * Sets pixels of extra space for layout child in invisible area.
+ *
+ * @param extraLayoutSpace Pixels of extra space for layout invisible child.
+ * Must be bigger or equals to 0.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setExtraLayoutSpace(int extraLayoutSpace) {
+ mLayoutManager.setExtraLayoutSpace(extraLayoutSpace);
+ }
+
+ /**
+ * Returns pixels of extra space for layout child in invisible area.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public int getExtraLayoutSpace() {
+ return mLayoutManager.getExtraLayoutSpace();
+ }
+
+ /**
+ * Temporarily slide out child views to bottom (for VerticalGridView) or end
+ * (for HorizontalGridView). Layout and scrolling will be suppressed until
+ * {@link #animateIn()} is called.
+ */
+ public void animateOut() {
+ mLayoutManager.slideOut();
+ }
+
+ /**
+ * Undo animateOut() and slide in child views.
+ */
+ public void animateIn() {
+ mLayoutManager.slideIn();
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ // dont abort the animateOut() animation, just record the position
+ if (mLayoutManager.isSlidingChildViews()) {
+ mLayoutManager.setSelectionWithSub(position, 0, 0);
+ return;
+ }
+ super.scrollToPosition(position);
+ }
+
+ @Override
+ public void smoothScrollToPosition(int position) {
+ // dont abort the animateOut() animation, just record the position
+ if (mLayoutManager.isSlidingChildViews()) {
+ mLayoutManager.setSelectionWithSub(position, 0, 0);
+ return;
+ }
+ super.smoothScrollToPosition(position);
+ }
+
+ /**
+ * Sets the number of items to prefetch in
+ * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
+ * which defines how many inner items should be prefetched when this GridView is nested inside
+ * another RecyclerView.
+ *
+ * <p>Set this value to the number of items this inner GridView will display when it is
+ * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items
+ * so they are ready, avoiding jank as the inner GridView is scrolled into the viewport.</p>
+ *
+ * <p>For example, take a VerticalGridView of scrolling HorizontalGridViews. The rows always
+ * have 6 items visible in them (or 7 if not aligned). Passing <code>6</code> to this method
+ * for each inner GridView will enable RecyclerView's prefetching feature to do create/bind work
+ * for 6 views within a row early, before it is scrolled on screen, instead of just the default
+ * 4.</p>
+ *
+ * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView
+ * nested in another RecyclerView.</p>
+ *
+ * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of
+ * views that will be visible in this view can incur unnecessary bind work, and an increase to
+ * the number of Views created and in active use.</p>
+ *
+ * @param itemCount Number of items to prefetch
+ *
+ * @see #getInitialPrefetchItemCount()
+ * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
+ * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
+ */
+ public void setInitialPrefetchItemCount(int itemCount) {
+ mInitialPrefetchItemCount = itemCount;
+ }
+
+ /**
+ * Gets the number of items to prefetch in
+ * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
+ * which defines how many inner items should be prefetched when this GridView is nested inside
+ * another RecyclerView.
+ *
+ * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
+ * @see #setInitialPrefetchItemCount(int)
+ * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
+ *
+ * @return number of items to prefetch.
+ */
+ public int getInitialPrefetchItemCount() {
+ return mInitialPrefetchItemCount;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java b/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java b/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
rename to leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java b/leanback/src/android/support/v17/leanback/widget/ControlBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java
rename to leanback/src/android/support/v17/leanback/widget/ControlBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java b/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java b/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DiffCallback.java b/leanback/src/android/support/v17/leanback/widget/DiffCallback.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DiffCallback.java
rename to leanback/src/android/support/v17/leanback/widget/DiffCallback.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java b/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DividerRow.java b/leanback/src/android/support/v17/leanback/widget/DividerRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DividerRow.java
rename to leanback/src/android/support/v17/leanback/widget/DividerRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FacetProvider.java b/leanback/src/android/support/v17/leanback/widget/FacetProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FacetProvider.java
rename to leanback/src/android/support/v17/leanback/widget/FacetProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java b/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java b/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java b/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
rename to leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java b/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
rename to leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java b/leanback/src/android/support/v17/leanback/widget/Grid.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Grid.java
rename to leanback/src/android/support/v17/leanback/widget/Grid.java
diff --git a/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
new file mode 100644
index 0000000..d7020e9
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -0,0 +1,3771 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import static android.support.v7.widget.RecyclerView.HORIZONTAL;
+import static android.support.v7.widget.RecyclerView.NO_ID;
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
+import static android.support.v7.widget.RecyclerView.VERTICAL;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.TraceCompat;
+import android.support.v4.util.CircularIntArray;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.OrientationHelper;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Recycler;
+import android.support.v7.widget.RecyclerView.State;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+final class GridLayoutManager extends RecyclerView.LayoutManager {
+
+ /*
+ * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
+ * The class currently does two internal jobs:
+ * - Saves optical bounds insets.
+ * - Caches focus align view center.
+ */
+ final static class LayoutParams extends RecyclerView.LayoutParams {
+
+ // For placement
+ int mLeftInset;
+ int mTopInset;
+ int mRightInset;
+ int mBottomInset;
+
+ // For alignment
+ private int mAlignX;
+ private int mAlignY;
+ private int[] mAlignMultiple;
+ private ItemAlignmentFacet mAlignmentFacet;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(RecyclerView.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ }
+
+ int getAlignX() {
+ return mAlignX;
+ }
+
+ int getAlignY() {
+ return mAlignY;
+ }
+
+ int getOpticalLeft(View view) {
+ return view.getLeft() + mLeftInset;
+ }
+
+ int getOpticalTop(View view) {
+ return view.getTop() + mTopInset;
+ }
+
+ int getOpticalRight(View view) {
+ return view.getRight() - mRightInset;
+ }
+
+ int getOpticalBottom(View view) {
+ return view.getBottom() - mBottomInset;
+ }
+
+ int getOpticalWidth(View view) {
+ return view.getWidth() - mLeftInset - mRightInset;
+ }
+
+ int getOpticalHeight(View view) {
+ return view.getHeight() - mTopInset - mBottomInset;
+ }
+
+ int getOpticalLeftInset() {
+ return mLeftInset;
+ }
+
+ int getOpticalRightInset() {
+ return mRightInset;
+ }
+
+ int getOpticalTopInset() {
+ return mTopInset;
+ }
+
+ int getOpticalBottomInset() {
+ return mBottomInset;
+ }
+
+ void setAlignX(int alignX) {
+ mAlignX = alignX;
+ }
+
+ void setAlignY(int alignY) {
+ mAlignY = alignY;
+ }
+
+ void setItemAlignmentFacet(ItemAlignmentFacet facet) {
+ mAlignmentFacet = facet;
+ }
+
+ ItemAlignmentFacet getItemAlignmentFacet() {
+ return mAlignmentFacet;
+ }
+
+ void calculateItemAlignments(int orientation, View view) {
+ ItemAlignmentFacet.ItemAlignmentDef[] defs = mAlignmentFacet.getAlignmentDefs();
+ if (mAlignMultiple == null || mAlignMultiple.length != defs.length) {
+ mAlignMultiple = new int[defs.length];
+ }
+ for (int i = 0; i < defs.length; i++) {
+ mAlignMultiple[i] = ItemAlignmentFacetHelper
+ .getAlignmentPosition(view, defs[i], orientation);
+ }
+ if (orientation == HORIZONTAL) {
+ mAlignX = mAlignMultiple[0];
+ } else {
+ mAlignY = mAlignMultiple[0];
+ }
+ }
+
+ int[] getAlignMultiple() {
+ return mAlignMultiple;
+ }
+
+ void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
+ mLeftInset = leftInset;
+ mTopInset = topInset;
+ mRightInset = rightInset;
+ mBottomInset = bottomInset;
+ }
+
+ }
+
+ /**
+ * Base class which scrolls to selected view in onStop().
+ */
+ abstract class GridLinearSmoothScroller extends LinearSmoothScroller {
+ GridLinearSmoothScroller() {
+ super(mBaseGridView.getContext());
+ }
+
+ @Override
+ protected void onStop() {
+ // onTargetFound() may not be called if we hit the "wall" first or get cancelled.
+ View targetView = findViewByPosition(getTargetPosition());
+ if (targetView == null) {
+ if (getTargetPosition() >= 0) {
+ // if smooth scroller is stopped without target, immediately jumps
+ // to the target position.
+ scrollToSelection(getTargetPosition(), 0, false, 0);
+ }
+ super.onStop();
+ return;
+ }
+ if (mFocusPosition != getTargetPosition()) {
+ // This should not happen since we cropped value in startPositionSmoothScroller()
+ mFocusPosition = getTargetPosition();
+ }
+ if (hasFocus()) {
+ mFlag |= PF_IN_SELECTION;
+ targetView.requestFocus();
+ mFlag &= ~PF_IN_SELECTION;
+ }
+ dispatchChildSelected();
+ dispatchChildSelectedAndPositioned();
+ super.onStop();
+ }
+
+ @Override
+ protected int calculateTimeForScrolling(int dx) {
+ int ms = super.calculateTimeForScrolling(dx);
+ if (mWindowAlignment.mainAxis().getSize() > 0) {
+ float minMs = (float) MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN
+ / mWindowAlignment.mainAxis().getSize() * dx;
+ if (ms < minMs) {
+ ms = (int) minMs;
+ }
+ }
+ return ms;
+ }
+
+ @Override
+ protected void onTargetFound(View targetView,
+ RecyclerView.State state, Action action) {
+ if (getScrollPosition(targetView, null, sTwoInts)) {
+ int dx, dy;
+ if (mOrientation == HORIZONTAL) {
+ dx = sTwoInts[0];
+ dy = sTwoInts[1];
+ } else {
+ dx = sTwoInts[1];
+ dy = sTwoInts[0];
+ }
+ final int distance = (int) Math.sqrt(dx * dx + dy * dy);
+ final int time = calculateTimeForDeceleration(distance);
+ action.update(dx, dy, time, mDecelerateInterpolator);
+ }
+ }
+ }
+
+ /**
+ * The SmoothScroller that remembers pending DPAD keys and consume pending keys
+ * during scroll.
+ */
+ final class PendingMoveSmoothScroller extends GridLinearSmoothScroller {
+ // -2 is a target position that LinearSmoothScroller can never find until
+ // consumePendingMovesXXX() sets real targetPosition.
+ final static int TARGET_UNDEFINED = -2;
+ // whether the grid is staggered.
+ private final boolean mStaggeredGrid;
+ // Number of pending movements on primary direction, negative if PREV_ITEM.
+ private int mPendingMoves;
+
+ PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid) {
+ mPendingMoves = initialPendingMoves;
+ mStaggeredGrid = staggeredGrid;
+ setTargetPosition(TARGET_UNDEFINED);
+ }
+
+ void increasePendingMoves() {
+ if (mPendingMoves < mMaxPendingMoves) {
+ mPendingMoves++;
+ }
+ }
+
+ void decreasePendingMoves() {
+ if (mPendingMoves > -mMaxPendingMoves) {
+ mPendingMoves--;
+ }
+ }
+
+ /**
+ * Called before laid out an item when non-staggered grid can handle pending movements
+ * by skipping "mNumRows" per movement; staggered grid will have to wait the item
+ * has been laid out in consumePendingMovesAfterLayout().
+ */
+ void consumePendingMovesBeforeLayout() {
+ if (mStaggeredGrid || mPendingMoves == 0) {
+ return;
+ }
+ View newSelected = null;
+ int startPos = mPendingMoves > 0 ? mFocusPosition + mNumRows :
+ mFocusPosition - mNumRows;
+ for (int pos = startPos; mPendingMoves != 0;
+ pos = mPendingMoves > 0 ? pos + mNumRows: pos - mNumRows) {
+ View v = findViewByPosition(pos);
+ if (v == null) {
+ break;
+ }
+ if (!canScrollTo(v)) {
+ continue;
+ }
+ newSelected = v;
+ mFocusPosition = pos;
+ mSubFocusPosition = 0;
+ if (mPendingMoves > 0) {
+ mPendingMoves--;
+ } else {
+ mPendingMoves++;
+ }
+ }
+ if (newSelected != null && hasFocus()) {
+ mFlag |= PF_IN_SELECTION;
+ newSelected.requestFocus();
+ mFlag &= ~PF_IN_SELECTION;
+ }
+ }
+
+ /**
+ * Called after laid out an item. Staggered grid should find view on same
+ * Row and consume pending movements.
+ */
+ void consumePendingMovesAfterLayout() {
+ if (mStaggeredGrid && mPendingMoves != 0) {
+ // consume pending moves, focus to item on the same row.
+ mPendingMoves = processSelectionMoves(true, mPendingMoves);
+ }
+ if (mPendingMoves == 0 || (mPendingMoves > 0 && hasCreatedLastItem())
+ || (mPendingMoves < 0 && hasCreatedFirstItem())) {
+ setTargetPosition(mFocusPosition);
+ stop();
+ }
+ }
+
+ @Override
+ protected void updateActionForInterimTarget(Action action) {
+ if (mPendingMoves == 0) {
+ return;
+ }
+ super.updateActionForInterimTarget(action);
+ }
+
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (mPendingMoves == 0) {
+ return null;
+ }
+ int direction = ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? mPendingMoves > 0 : mPendingMoves < 0)
+ ? -1 : 1;
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ // if we hit wall, need clear the remaining pending moves.
+ mPendingMoves = 0;
+ mPendingMoveSmoothScroller = null;
+ View v = findViewByPosition(getTargetPosition());
+ if (v != null) scrollToView(v, true);
+ }
+ };
+
+ private static final String TAG = "GridLayoutManager";
+ static final boolean DEBUG = false;
+ static final boolean TRACE = false;
+
+ // maximum pending movement in one direction.
+ static final int DEFAULT_MAX_PENDING_MOVES = 10;
+ int mMaxPendingMoves = DEFAULT_MAX_PENDING_MOVES;
+ // minimal milliseconds to scroll window size in major direction, we put a cap to prevent the
+ // effect smooth scrolling too over to bind an item view then drag the item view back.
+ final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
+
+ String getTag() {
+ return TAG + ":" + mBaseGridView.getId();
+ }
+
+ final BaseGridView mBaseGridView;
+
+ /**
+ * Note on conventions in the presence of RTL layout directions:
+ * Many properties and method names reference entities related to the
+ * beginnings and ends of things. In the presence of RTL flows,
+ * it may not be clear whether this is intended to reference a
+ * quantity that changes direction in RTL cases, or a quantity that
+ * does not. Here are the conventions in use:
+ *
+ * start/end: coordinate quantities - do reverse
+ * (optical) left/right: coordinate quantities - do not reverse
+ * low/high: coordinate quantities - do not reverse
+ * min/max: coordinate quantities - do not reverse
+ * scroll offset - coordinate quantities - do not reverse
+ * first/last: positional indices - do not reverse
+ * front/end: positional indices - do not reverse
+ * prepend/append: related to positional indices - do not reverse
+ *
+ * Note that although quantities do not reverse in RTL flows, their
+ * relationship does. In LTR flows, the first positional index is
+ * leftmost; in RTL flows, it is rightmost. Thus, anywhere that
+ * positional quantities are mapped onto coordinate quantities,
+ * the flow must be checked and the logic reversed.
+ */
+
+ /**
+ * The orientation of a "row".
+ */
+ @RecyclerView.Orientation
+ int mOrientation = HORIZONTAL;
+ private OrientationHelper mOrientationHelper = OrientationHelper.createHorizontalHelper(this);
+
+ RecyclerView.State mState;
+ // Suppose currently showing 4, 5, 6, 7; removing 2,3,4 will make the layoutPosition to be
+ // 2(deleted), 3, 4, 5 in prelayout pass. So when we add item in prelayout, we must subtract 2
+ // from index of Grid.createItem.
+ int mPositionDeltaInPreLayout;
+ // Extra layout space needs to fill in prelayout pass. Note we apply the extra space to both
+ // appends and prepends due to the fact leanback is doing mario scrolling: removing items to
+ // the left of focused item might need extra layout on the right.
+ int mExtraLayoutSpaceInPreLayout;
+ // mPositionToRowInPostLayout and mDisappearingPositions are temp variables in post layout.
+ final SparseIntArray mPositionToRowInPostLayout = new SparseIntArray();
+ int[] mDisappearingPositions;
+
+ RecyclerView.Recycler mRecycler;
+
+ private static final Rect sTempRect = new Rect();
+
+ // 2 bits mask is for 3 STAGEs: 0, PF_STAGE_LAYOUT or PF_STAGE_SCROLL.
+ static final int PF_STAGE_MASK = 0x3;
+ static final int PF_STAGE_LAYOUT = 0x1;
+ static final int PF_STAGE_SCROLL = 0x2;
+
+ // Flag for "in fast relayout", determined by layoutInit() result.
+ static final int PF_FAST_RELAYOUT = 1 << 2;
+
+ // Flag for the selected item being updated in fast relayout.
+ static final int PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION = 1 << 3;
+ /**
+ * During full layout pass, when GridView had focus: onLayoutChildren will
+ * skip non-focusable child and adjust mFocusPosition.
+ */
+ static final int PF_IN_LAYOUT_SEARCH_FOCUS = 1 << 4;
+
+ // flag to prevent reentry if it's already processing selection request.
+ static final int PF_IN_SELECTION = 1 << 5;
+
+ // Represents whether child views are temporarily sliding out
+ static final int PF_SLIDING = 1 << 6;
+ static final int PF_LAYOUT_EATEN_IN_SLIDING = 1 << 7;
+
+ /**
+ * Force a full layout under certain situations. E.g. Rows change, jump to invisible child.
+ */
+ static final int PF_FORCE_FULL_LAYOUT = 1 << 8;
+
+ /**
+ * True if layout is enabled.
+ */
+ static final int PF_LAYOUT_ENABLED = 1 << 9;
+
+ /**
+ * Flag controlling whether the current/next layout should
+ * be updating the secondary size of rows.
+ */
+ static final int PF_ROW_SECONDARY_SIZE_REFRESH = 1 << 10;
+
+ /**
+ * Allow DPAD key to navigate out at the front of the View (where position = 0),
+ * default is false.
+ */
+ static final int PF_FOCUS_OUT_FRONT = 1 << 11;
+
+ /**
+ * Allow DPAD key to navigate out at the end of the view, default is false.
+ */
+ static final int PF_FOCUS_OUT_END = 1 << 12;
+
+ static final int PF_FOCUS_OUT_MASKS = PF_FOCUS_OUT_FRONT | PF_FOCUS_OUT_END;
+
+ /**
+ * Allow DPAD key to navigate out of second axis.
+ * default is true.
+ */
+ static final int PF_FOCUS_OUT_SIDE_START = 1 << 13;
+
+ /**
+ * Allow DPAD key to navigate out of second axis.
+ */
+ static final int PF_FOCUS_OUT_SIDE_END = 1 << 14;
+
+ static final int PF_FOCUS_OUT_SIDE_MASKS = PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END;
+
+ /**
+ * True if focus search is disabled.
+ */
+ static final int PF_FOCUS_SEARCH_DISABLED = 1 << 15;
+
+ /**
+ * True if prune child, might be disabled during transition.
+ */
+ static final int PF_PRUNE_CHILD = 1 << 16;
+
+ /**
+ * True if scroll content, might be disabled during transition.
+ */
+ static final int PF_SCROLL_ENABLED = 1 << 17;
+
+ /**
+ * Set to true for RTL layout in horizontal orientation
+ */
+ static final int PF_REVERSE_FLOW_PRIMARY = 1 << 18;
+
+ /**
+ * Set to true for RTL layout in vertical orientation
+ */
+ static final int PF_REVERSE_FLOW_SECONDARY = 1 << 19;
+
+ static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY;
+
+ int mFlag = PF_LAYOUT_ENABLED
+ | PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END
+ | PF_PRUNE_CHILD | PF_SCROLL_ENABLED;
+
+ private OnChildSelectedListener mChildSelectedListener = null;
+
+ private ArrayList<OnChildViewHolderSelectedListener> mChildViewHolderSelectedListeners = null;
+
+ OnChildLaidOutListener mChildLaidOutListener = null;
+
+ /**
+ * The focused position, it's not the currently visually aligned position
+ * but it is the final position that we intend to focus on. If there are
+ * multiple setSelection() called, mFocusPosition saves last value.
+ */
+ int mFocusPosition = NO_POSITION;
+
+ /**
+ * A view can have multiple alignment position, this is the index of which
+ * alignment is used, by default is 0.
+ */
+ int mSubFocusPosition = 0;
+
+ /**
+ * LinearSmoothScroller that consume pending DPAD movements.
+ */
+ PendingMoveSmoothScroller mPendingMoveSmoothScroller;
+
+ /**
+ * The offset to be applied to mFocusPosition, due to adapter change, on the next
+ * layout. Set to Integer.MIN_VALUE means we should stop adding delta to mFocusPosition
+ * until next layout cycler.
+ * TODO: This is somewhat duplication of RecyclerView getOldPosition() which is
+ * unfortunately cleared after prelayout.
+ */
+ private int mFocusPositionOffset = 0;
+
+ /**
+ * Extra pixels applied on primary direction.
+ */
+ private int mPrimaryScrollExtra;
+
+ /**
+ * override child visibility
+ */
+ @Visibility
+ int mChildVisibility;
+
+ /**
+ * Pixels that scrolled in secondary forward direction. Negative value means backward.
+ * Note that we treat secondary differently than main. For the main axis, update scroll min/max
+ * based on first/last item's view location. For second axis, we don't use item's view location.
+ * We are using the {@link #getRowSizeSecondary(int)} plus mScrollOffsetSecondary. see
+ * details in {@link #updateSecondaryScrollLimits()}.
+ */
+ int mScrollOffsetSecondary;
+
+ /**
+ * User-specified row height/column width. Can be WRAP_CONTENT.
+ */
+ private int mRowSizeSecondaryRequested;
+
+ /**
+ * The fixed size of each grid item in the secondary direction. This corresponds to
+ * the row height, equal for all rows. Grid items may have variable length
+ * in the primary direction.
+ */
+ private int mFixedRowSizeSecondary;
+
+ /**
+ * Tracks the secondary size of each row.
+ */
+ private int[] mRowSizeSecondary;
+
+ /**
+ * The maximum measured size of the view.
+ */
+ private int mMaxSizeSecondary;
+
+ /**
+ * Margin between items.
+ */
+ private int mHorizontalSpacing;
+ /**
+ * Margin between items vertically.
+ */
+ private int mVerticalSpacing;
+ /**
+ * Margin in main direction.
+ */
+ private int mSpacingPrimary;
+ /**
+ * Margin in second direction.
+ */
+ private int mSpacingSecondary;
+ /**
+ * How to position child in secondary direction.
+ */
+ private int mGravity = Gravity.START | Gravity.TOP;
+ /**
+ * The number of rows in the grid.
+ */
+ int mNumRows;
+ /**
+ * Number of rows requested, can be 0 to be determined by parent size and
+ * rowHeight.
+ */
+ private int mNumRowsRequested = 1;
+
+ /**
+ * Saves grid information of each view.
+ */
+ Grid mGrid;
+
+ /**
+ * Focus Scroll strategy.
+ */
+ private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
+ /**
+ * Defines how item view is aligned in the window.
+ */
+ final WindowAlignment mWindowAlignment = new WindowAlignment();
+
+ /**
+ * Defines how item view is aligned.
+ */
+ private final ItemAlignment mItemAlignment = new ItemAlignment();
+
+ /**
+ * Dimensions of the view, width or height depending on orientation.
+ */
+ private int mSizePrimary;
+
+ /**
+ * Pixels of extra space for layout item (outside the widget)
+ */
+ private int mExtraLayoutSpace;
+
+ /**
+ * Temporary variable: an int array of length=2.
+ */
+ static int[] sTwoInts = new int[2];
+
+ /**
+ * Temporaries used for measuring.
+ */
+ private int[] mMeasuredDimension = new int[2];
+
+ final ViewsStateBundle mChildrenStates = new ViewsStateBundle();
+
+ /**
+ * Optional interface implemented by Adapter.
+ */
+ private FacetProviderAdapter mFacetProviderAdapter;
+
+ public GridLayoutManager(BaseGridView baseGridView) {
+ mBaseGridView = baseGridView;
+ mChildVisibility = -1;
+ // disable prefetch by default, prefetch causes regression on low power chipset
+ setItemPrefetchEnabled(false);
+ }
+
+ public void setOrientation(@RecyclerView.Orientation int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
+ return;
+ }
+
+ mOrientation = orientation;
+ mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
+ mWindowAlignment.setOrientation(orientation);
+ mItemAlignment.setOrientation(orientation);
+ mFlag |= PF_FORCE_FULL_LAYOUT;
+ }
+
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ final int flags;
+ if (mOrientation == HORIZONTAL) {
+ flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_PRIMARY : 0;
+ } else {
+ flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_SECONDARY : 0;
+ }
+ if ((mFlag & PF_REVERSE_FLOW_MASK) == flags) {
+ return;
+ }
+ mFlag = (mFlag & ~PF_REVERSE_FLOW_MASK) | flags;
+ mFlag |= PF_FORCE_FULL_LAYOUT;
+ mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
+ }
+
+ public int getFocusScrollStrategy() {
+ return mFocusScrollStrategy;
+ }
+
+ public void setFocusScrollStrategy(int focusScrollStrategy) {
+ mFocusScrollStrategy = focusScrollStrategy;
+ }
+
+ public void setWindowAlignment(int windowAlignment) {
+ mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
+ }
+
+ public int getWindowAlignment() {
+ return mWindowAlignment.mainAxis().getWindowAlignment();
+ }
+
+ public void setWindowAlignmentOffset(int alignmentOffset) {
+ mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
+ }
+
+ public int getWindowAlignmentOffset() {
+ return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
+ }
+
+ public void setWindowAlignmentOffsetPercent(float offsetPercent) {
+ mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
+ }
+
+ public float getWindowAlignmentOffsetPercent() {
+ return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
+ }
+
+ public void setItemAlignmentOffset(int alignmentOffset) {
+ mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
+ updateChildAlignments();
+ }
+
+ public int getItemAlignmentOffset() {
+ return mItemAlignment.mainAxis().getItemAlignmentOffset();
+ }
+
+ public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
+ mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
+ updateChildAlignments();
+ }
+
+ public boolean isItemAlignmentOffsetWithPadding() {
+ return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
+ }
+
+ public void setItemAlignmentOffsetPercent(float offsetPercent) {
+ mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
+ updateChildAlignments();
+ }
+
+ public float getItemAlignmentOffsetPercent() {
+ return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
+ }
+
+ public void setItemAlignmentViewId(int viewId) {
+ mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
+ updateChildAlignments();
+ }
+
+ public int getItemAlignmentViewId() {
+ return mItemAlignment.mainAxis().getItemAlignmentViewId();
+ }
+
+ public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
+ mFlag = (mFlag & ~PF_FOCUS_OUT_MASKS)
+ | (throughFront ? PF_FOCUS_OUT_FRONT : 0)
+ | (throughEnd ? PF_FOCUS_OUT_END : 0);
+ }
+
+ public void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) {
+ mFlag = (mFlag & ~PF_FOCUS_OUT_SIDE_MASKS)
+ | (throughStart ? PF_FOCUS_OUT_SIDE_START : 0)
+ | (throughEnd ? PF_FOCUS_OUT_SIDE_END : 0);
+ }
+
+ public void setNumRows(int numRows) {
+ if (numRows < 0) throw new IllegalArgumentException();
+ mNumRowsRequested = numRows;
+ }
+
+ /**
+ * Set the row height. May be WRAP_CONTENT, or a size in pixels.
+ */
+ public void setRowHeight(int height) {
+ if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mRowSizeSecondaryRequested = height;
+ } else {
+ throw new IllegalArgumentException("Invalid row height: " + height);
+ }
+ }
+
+ public void setItemSpacing(int space) {
+ mVerticalSpacing = mHorizontalSpacing = space;
+ mSpacingPrimary = mSpacingSecondary = space;
+ }
+
+ public void setVerticalSpacing(int space) {
+ if (mOrientation == VERTICAL) {
+ mSpacingPrimary = mVerticalSpacing = space;
+ } else {
+ mSpacingSecondary = mVerticalSpacing = space;
+ }
+ }
+
+ public void setHorizontalSpacing(int space) {
+ if (mOrientation == HORIZONTAL) {
+ mSpacingPrimary = mHorizontalSpacing = space;
+ } else {
+ mSpacingSecondary = mHorizontalSpacing = space;
+ }
+ }
+
+ public int getVerticalSpacing() {
+ return mVerticalSpacing;
+ }
+
+ public int getHorizontalSpacing() {
+ return mHorizontalSpacing;
+ }
+
+ public void setGravity(int gravity) {
+ mGravity = gravity;
+ }
+
+ protected boolean hasDoneFirstLayout() {
+ return mGrid != null;
+ }
+
+ public void setOnChildSelectedListener(OnChildSelectedListener listener) {
+ mChildSelectedListener = listener;
+ }
+
+ public void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
+ if (listener == null) {
+ mChildViewHolderSelectedListeners = null;
+ return;
+ }
+ if (mChildViewHolderSelectedListeners == null) {
+ mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>();
+ } else {
+ mChildViewHolderSelectedListeners.clear();
+ }
+ mChildViewHolderSelectedListeners.add(listener);
+ }
+
+ public void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
+ if (mChildViewHolderSelectedListeners == null) {
+ mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>();
+ }
+ mChildViewHolderSelectedListeners.add(listener);
+ }
+
+ public void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener
+ listener) {
+ if (mChildViewHolderSelectedListeners != null) {
+ mChildViewHolderSelectedListeners.remove(listener);
+ }
+ }
+
+ boolean hasOnChildViewHolderSelectedListener() {
+ return mChildViewHolderSelectedListeners != null
+ && mChildViewHolderSelectedListeners.size() > 0;
+ }
+
+ void fireOnChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child,
+ int position, int subposition) {
+ if (mChildViewHolderSelectedListeners == null) {
+ return;
+ }
+ for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0 ; i--) {
+ mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelected(parent, child,
+ position, subposition);
+ }
+ }
+
+ void fireOnChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder
+ child, int position, int subposition) {
+ if (mChildViewHolderSelectedListeners == null) {
+ return;
+ }
+ for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0 ; i--) {
+ mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelectedAndPositioned(parent,
+ child, position, subposition);
+ }
+ }
+
+ void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
+ mChildLaidOutListener = listener;
+ }
+
+ private int getAdapterPositionByView(View view) {
+ if (view == null) {
+ return NO_POSITION;
+ }
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ if (params == null || params.isItemRemoved()) {
+ // when item is removed, the position value can be any value.
+ return NO_POSITION;
+ }
+ return params.getViewAdapterPosition();
+ }
+
+ int getSubPositionByView(View view, View childView) {
+ if (view == null || childView == null) {
+ return 0;
+ }
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final ItemAlignmentFacet facet = lp.getItemAlignmentFacet();
+ if (facet != null) {
+ final ItemAlignmentFacet.ItemAlignmentDef[] defs = facet.getAlignmentDefs();
+ if (defs.length > 1) {
+ while (childView != view) {
+ int id = childView.getId();
+ if (id != View.NO_ID) {
+ for (int i = 1; i < defs.length; i++) {
+ if (defs[i].getItemAlignmentFocusViewId() == id) {
+ return i;
+ }
+ }
+ }
+ childView = (View) childView.getParent();
+ }
+ }
+ }
+ return 0;
+ }
+
+ private int getAdapterPositionByIndex(int index) {
+ return getAdapterPositionByView(getChildAt(index));
+ }
+
+ void dispatchChildSelected() {
+ if (mChildSelectedListener == null && !hasOnChildViewHolderSelectedListener()) {
+ return;
+ }
+
+ if (TRACE) TraceCompat.beginSection("onChildSelected");
+ View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
+ if (view != null) {
+ RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
+ if (mChildSelectedListener != null) {
+ mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
+ vh == null? NO_ID: vh.getItemId());
+ }
+ fireOnChildViewHolderSelected(mBaseGridView, vh, mFocusPosition, mSubFocusPosition);
+ } else {
+ if (mChildSelectedListener != null) {
+ mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
+ }
+ fireOnChildViewHolderSelected(mBaseGridView, null, NO_POSITION, 0);
+ }
+ if (TRACE) TraceCompat.endSection();
+
+ // Children may request layout when a child selection event occurs (such as a change of
+ // padding on the current and previously selected rows).
+ // If in layout, a child requesting layout may have been laid out before the selection
+ // callback.
+ // If it was not, the child will be laid out after the selection callback.
+ // If so, the layout request will be honoured though the view system will emit a double-
+ // layout warning.
+ // If not in layout, we may be scrolling in which case the child layout request will be
+ // eaten by recyclerview. Post a requestLayout.
+ if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && !mBaseGridView.isLayoutRequested()) {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i).isLayoutRequested()) {
+ forceRequestLayout();
+ break;
+ }
+ }
+ }
+ }
+
+ private void dispatchChildSelectedAndPositioned() {
+ if (!hasOnChildViewHolderSelectedListener()) {
+ return;
+ }
+
+ if (TRACE) TraceCompat.beginSection("onChildSelectedAndPositioned");
+ View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
+ if (view != null) {
+ RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
+ fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, vh, mFocusPosition,
+ mSubFocusPosition);
+ } else {
+ if (mChildSelectedListener != null) {
+ mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
+ }
+ fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, null, NO_POSITION, 0);
+ }
+ if (TRACE) TraceCompat.endSection();
+
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ // We can scroll horizontally if we have horizontal orientation, or if
+ // we are vertical and have more than one column.
+ return mOrientation == HORIZONTAL || mNumRows > 1;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ // We can scroll vertically if we have vertical orientation, or if we
+ // are horizontal and have more than one row.
+ return mOrientation == VERTICAL || mNumRows > 1;
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
+ return new LayoutParams(context, attrs);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof RecyclerView.LayoutParams) {
+ return new LayoutParams((RecyclerView.LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ protected View getViewForPosition(int position) {
+ return mRecycler.getViewForPosition(position);
+ }
+
+ final int getOpticalLeft(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
+ }
+
+ final int getOpticalRight(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
+ }
+
+ final int getOpticalTop(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
+ }
+
+ final int getOpticalBottom(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
+ }
+
+ @Override
+ public int getDecoratedLeft(View child) {
+ return super.getDecoratedLeft(child) + ((LayoutParams) child.getLayoutParams()).mLeftInset;
+ }
+
+ @Override
+ public int getDecoratedTop(View child) {
+ return super.getDecoratedTop(child) + ((LayoutParams) child.getLayoutParams()).mTopInset;
+ }
+
+ @Override
+ public int getDecoratedRight(View child) {
+ return super.getDecoratedRight(child)
+ - ((LayoutParams) child.getLayoutParams()).mRightInset;
+ }
+
+ @Override
+ public int getDecoratedBottom(View child) {
+ return super.getDecoratedBottom(child)
+ - ((LayoutParams) child.getLayoutParams()).mBottomInset;
+ }
+
+ @Override
+ public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
+ super.getDecoratedBoundsWithMargins(view, outBounds);
+ LayoutParams params = ((LayoutParams) view.getLayoutParams());
+ outBounds.left += params.mLeftInset;
+ outBounds.top += params.mTopInset;
+ outBounds.right -= params.mRightInset;
+ outBounds.bottom -= params.mBottomInset;
+ }
+
+ int getViewMin(View v) {
+ return mOrientationHelper.getDecoratedStart(v);
+ }
+
+ int getViewMax(View v) {
+ return mOrientationHelper.getDecoratedEnd(v);
+ }
+
+ int getViewPrimarySize(View view) {
+ getDecoratedBoundsWithMargins(view, sTempRect);
+ return mOrientation == HORIZONTAL ? sTempRect.width() : sTempRect.height();
+ }
+
+ private int getViewCenter(View view) {
+ return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
+ }
+
+ private int getAdjustedViewCenter(View view) {
+ if (view.hasFocus()) {
+ View child = view.findFocus();
+ if (child != null && child != view) {
+ return getAdjustedPrimaryAlignedScrollDistance(getViewCenter(view), view, child);
+ }
+ }
+ return getViewCenter(view);
+ }
+
+ private int getViewCenterSecondary(View view) {
+ return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
+ }
+
+ private int getViewCenterX(View v) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ return p.getOpticalLeft(v) + p.getAlignX();
+ }
+
+ private int getViewCenterY(View v) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ return p.getOpticalTop(v) + p.getAlignY();
+ }
+
+ /**
+ * Save Recycler and State for convenience. Must be paired with leaveContext().
+ */
+ private void saveContext(Recycler recycler, State state) {
+ if (mRecycler != null || mState != null) {
+ Log.e(TAG, "Recycler information was not released, bug!");
+ }
+ mRecycler = recycler;
+ mState = state;
+ mPositionDeltaInPreLayout = 0;
+ mExtraLayoutSpaceInPreLayout = 0;
+ }
+
+ /**
+ * Discard saved Recycler and State.
+ */
+ private void leaveContext() {
+ mRecycler = null;
+ mState = null;
+ mPositionDeltaInPreLayout = 0;
+ mExtraLayoutSpaceInPreLayout = 0;
+ }
+
+ /**
+ * Re-initialize data structures for a data change or handling invisible
+ * selection. The method tries its best to preserve position information so
+ * that staggered grid looks same before and after re-initialize.
+ * @return true if can fastRelayout()
+ */
+ private boolean layoutInit() {
+ final int newItemCount = mState.getItemCount();
+ if (newItemCount == 0) {
+ mFocusPosition = NO_POSITION;
+ mSubFocusPosition = 0;
+ } else if (mFocusPosition >= newItemCount) {
+ mFocusPosition = newItemCount - 1;
+ mSubFocusPosition = 0;
+ } else if (mFocusPosition == NO_POSITION && newItemCount > 0) {
+ // if focus position is never set before, initialize it to 0
+ mFocusPosition = 0;
+ mSubFocusPosition = 0;
+ }
+ if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
+ && (mFlag & PF_FORCE_FULL_LAYOUT) == 0 && mGrid.getNumRows() == mNumRows) {
+ updateScrollController();
+ updateSecondaryScrollLimits();
+ mGrid.setSpacing(mSpacingPrimary);
+ return true;
+ } else {
+ mFlag &= ~PF_FORCE_FULL_LAYOUT;
+
+ if (mGrid == null || mNumRows != mGrid.getNumRows()
+ || ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) != mGrid.isReversedFlow()) {
+ mGrid = Grid.createGrid(mNumRows);
+ mGrid.setProvider(mGridProvider);
+ mGrid.setReversedFlow((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0);
+ }
+ initScrollController();
+ updateSecondaryScrollLimits();
+ mGrid.setSpacing(mSpacingPrimary);
+ detachAndScrapAttachedViews(mRecycler);
+ mGrid.resetVisibleIndex();
+ mWindowAlignment.mainAxis().invalidateScrollMin();
+ mWindowAlignment.mainAxis().invalidateScrollMax();
+ return false;
+ }
+ }
+
+ private int getRowSizeSecondary(int rowIndex) {
+ if (mFixedRowSizeSecondary != 0) {
+ return mFixedRowSizeSecondary;
+ }
+ if (mRowSizeSecondary == null) {
+ return 0;
+ }
+ return mRowSizeSecondary[rowIndex];
+ }
+
+ int getRowStartSecondary(int rowIndex) {
+ int start = 0;
+ // Iterate from left to right, which is a different index traversal
+ // in RTL flow
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
+ for (int i = mNumRows-1; i > rowIndex; i--) {
+ start += getRowSizeSecondary(i) + mSpacingSecondary;
+ }
+ } else {
+ for (int i = 0; i < rowIndex; i++) {
+ start += getRowSizeSecondary(i) + mSpacingSecondary;
+ }
+ }
+ return start;
+ }
+
+ private int getSizeSecondary() {
+ int rightmostIndex = (mFlag & PF_REVERSE_FLOW_SECONDARY) != 0 ? 0 : mNumRows - 1;
+ return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
+ }
+
+ int getDecoratedMeasuredWidthWithMargin(View v) {
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ return getDecoratedMeasuredWidth(v) + lp.leftMargin + lp.rightMargin;
+ }
+
+ int getDecoratedMeasuredHeightWithMargin(View v) {
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ return getDecoratedMeasuredHeight(v) + lp.topMargin + lp.bottomMargin;
+ }
+
+ private void measureScrapChild(int position, int widthSpec, int heightSpec,
+ int[] measuredDimension) {
+ View view = mRecycler.getViewForPosition(position);
+ if (view != null) {
+ final LayoutParams p = (LayoutParams) view.getLayoutParams();
+ calculateItemDecorationsForChild(view, sTempRect);
+ int widthUsed = p.leftMargin + p.rightMargin + sTempRect.left + sTempRect.right;
+ int heightUsed = p.topMargin + p.bottomMargin + sTempRect.top + sTempRect.bottom;
+
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
+ getPaddingLeft() + getPaddingRight() + widthUsed, p.width);
+ int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
+ getPaddingTop() + getPaddingBottom() + heightUsed, p.height);
+ view.measure(childWidthSpec, childHeightSpec);
+
+ measuredDimension[0] = getDecoratedMeasuredWidthWithMargin(view);
+ measuredDimension[1] = getDecoratedMeasuredHeightWithMargin(view);
+ mRecycler.recycleView(view);
+ }
+ }
+
+ private boolean processRowSizeSecondary(boolean measure) {
+ if (mFixedRowSizeSecondary != 0 || mRowSizeSecondary == null) {
+ return false;
+ }
+
+ if (TRACE) TraceCompat.beginSection("processRowSizeSecondary");
+ CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
+ boolean changed = false;
+ int scrapeChildSize = -1;
+
+ for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
+ CircularIntArray row = rows == null ? null : rows[rowIndex];
+ final int rowItemsPairCount = row == null ? 0 : row.size();
+ int rowSize = -1;
+ for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
+ rowItemPairIndex += 2) {
+ final int rowIndexStart = row.get(rowItemPairIndex);
+ final int rowIndexEnd = row.get(rowItemPairIndex + 1);
+ for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
+ final View view = findViewByPosition(i - mPositionDeltaInPreLayout);
+ if (view == null) {
+ continue;
+ }
+ if (measure) {
+ measureChild(view);
+ }
+ final int secondarySize = mOrientation == HORIZONTAL
+ ? getDecoratedMeasuredHeightWithMargin(view)
+ : getDecoratedMeasuredWidthWithMargin(view);
+ if (secondarySize > rowSize) {
+ rowSize = secondarySize;
+ }
+ }
+ }
+
+ final int itemCount = mState.getItemCount();
+ if (!mBaseGridView.hasFixedSize() && measure && rowSize < 0 && itemCount > 0) {
+ if (scrapeChildSize < 0) {
+ // measure a child that is close to mFocusPosition but not currently visible
+ int position = mFocusPosition;
+ if (position < 0) {
+ position = 0;
+ } else if (position >= itemCount) {
+ position = itemCount - 1;
+ }
+ if (getChildCount() > 0) {
+ int firstPos = mBaseGridView.getChildViewHolder(
+ getChildAt(0)).getLayoutPosition();
+ int lastPos = mBaseGridView.getChildViewHolder(
+ getChildAt(getChildCount() - 1)).getLayoutPosition();
+ // if mFocusPosition is between first and last, choose either
+ // first - 1 or last + 1
+ if (position >= firstPos && position <= lastPos) {
+ position = (position - firstPos <= lastPos - position)
+ ? (firstPos - 1) : (lastPos + 1);
+ // try the other value if the position is invalid. if both values are
+ // invalid, skip measureScrapChild below.
+ if (position < 0 && lastPos < itemCount - 1) {
+ position = lastPos + 1;
+ } else if (position >= itemCount && firstPos > 0) {
+ position = firstPos - 1;
+ }
+ }
+ }
+ if (position >= 0 && position < itemCount) {
+ measureScrapChild(position,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ mMeasuredDimension);
+ scrapeChildSize = mOrientation == HORIZONTAL ? mMeasuredDimension[1] :
+ mMeasuredDimension[0];
+ if (DEBUG) {
+ Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] + " "
+ + mMeasuredDimension[1]);
+ }
+ }
+ }
+ if (scrapeChildSize >= 0) {
+ rowSize = scrapeChildSize;
+ }
+ }
+ if (rowSize < 0) {
+ rowSize = 0;
+ }
+ if (mRowSizeSecondary[rowIndex] != rowSize) {
+ if (DEBUG) {
+ Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex]
+ + ", " + rowSize);
+ }
+ mRowSizeSecondary[rowIndex] = rowSize;
+ changed = true;
+ }
+ }
+
+ if (TRACE) TraceCompat.endSection();
+ return changed;
+ }
+
+ /**
+ * Checks if we need to update row secondary sizes.
+ */
+ private void updateRowSecondarySizeRefresh() {
+ mFlag = (mFlag & ~PF_ROW_SECONDARY_SIZE_REFRESH)
+ | (processRowSizeSecondary(false) ? PF_ROW_SECONDARY_SIZE_REFRESH : 0);
+ if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
+ if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
+ forceRequestLayout();
+ }
+ }
+
+ private void forceRequestLayout() {
+ if (DEBUG) Log.v(getTag(), "forceRequestLayout");
+ // RecyclerView prevents us from requesting layout in many cases
+ // (during layout, during scroll, etc.)
+ // For secondary row size wrap_content support we currently need a
+ // second layout pass to update the measured size after having measured
+ // and added child views in layoutChildren.
+ // Force the second layout by posting a delayed runnable.
+ // TODO: investigate allowing a second layout pass,
+ // or move child add/measure logic to the measure phase.
+ ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
+ }
+
+ private final Runnable mRequestLayoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(getTag(), "request Layout from runnable");
+ requestLayout();
+ }
+ };
+
+ @Override
+ public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
+ saveContext(recycler, state);
+
+ int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
+ int measuredSizeSecondary;
+ if (mOrientation == HORIZONTAL) {
+ sizePrimary = MeasureSpec.getSize(widthSpec);
+ sizeSecondary = MeasureSpec.getSize(heightSpec);
+ modeSecondary = MeasureSpec.getMode(heightSpec);
+ paddingSecondary = getPaddingTop() + getPaddingBottom();
+ } else {
+ sizeSecondary = MeasureSpec.getSize(widthSpec);
+ sizePrimary = MeasureSpec.getSize(heightSpec);
+ modeSecondary = MeasureSpec.getMode(widthSpec);
+ paddingSecondary = getPaddingLeft() + getPaddingRight();
+ }
+ if (DEBUG) {
+ Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec)
+ + " heightSpec " + Integer.toHexString(heightSpec)
+ + " modeSecondary " + Integer.toHexString(modeSecondary)
+ + " sizeSecondary " + sizeSecondary + " " + this);
+ }
+
+ mMaxSizeSecondary = sizeSecondary;
+
+ if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
+ mFixedRowSizeSecondary = 0;
+
+ if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
+ mRowSizeSecondary = new int[mNumRows];
+ }
+
+ if (mState.isPreLayout()) {
+ updatePositionDeltaInPreLayout();
+ }
+ // Measure all current children and update cached row height or column width
+ processRowSizeSecondary(true);
+
+ switch (modeSecondary) {
+ case MeasureSpec.UNSPECIFIED:
+ measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
+ mMaxSizeSecondary);
+ break;
+ case MeasureSpec.EXACTLY:
+ measuredSizeSecondary = mMaxSizeSecondary;
+ break;
+ default:
+ throw new IllegalStateException("wrong spec");
+ }
+
+ } else {
+ switch (modeSecondary) {
+ case MeasureSpec.UNSPECIFIED:
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested == 0
+ ? sizeSecondary - paddingSecondary : mRowSizeSecondaryRequested;
+ mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
+ measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
+ * (mNumRows - 1) + paddingSecondary;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
+ mNumRows = 1;
+ mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
+ } else if (mNumRowsRequested == 0) {
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
+ mNumRows = (sizeSecondary + mSpacingSecondary)
+ / (mRowSizeSecondaryRequested + mSpacingSecondary);
+ } else if (mRowSizeSecondaryRequested == 0) {
+ mNumRows = mNumRowsRequested;
+ mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary
+ - mSpacingSecondary * (mNumRows - 1)) / mNumRows;
+ } else {
+ mNumRows = mNumRowsRequested;
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
+ }
+ measuredSizeSecondary = sizeSecondary;
+ if (modeSecondary == MeasureSpec.AT_MOST) {
+ int childrenSize = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
+ * (mNumRows - 1) + paddingSecondary;
+ if (childrenSize < measuredSizeSecondary) {
+ measuredSizeSecondary = childrenSize;
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException("wrong spec");
+ }
+ }
+ if (mOrientation == HORIZONTAL) {
+ setMeasuredDimension(sizePrimary, measuredSizeSecondary);
+ } else {
+ setMeasuredDimension(measuredSizeSecondary, sizePrimary);
+ }
+ if (DEBUG) {
+ Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary
+ + " measuredSizeSecondary " + measuredSizeSecondary
+ + " mFixedRowSizeSecondary " + mFixedRowSizeSecondary
+ + " mNumRows " + mNumRows);
+ }
+ leaveContext();
+ }
+
+ void measureChild(View child) {
+ if (TRACE) TraceCompat.beginSection("measureChild");
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ calculateItemDecorationsForChild(child, sTempRect);
+ int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right;
+ int heightUsed = lp.topMargin + lp.bottomMargin + sTempRect.top + sTempRect.bottom;
+
+ final int secondarySpec =
+ (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT)
+ ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ : MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
+ int widthSpec, heightSpec;
+
+ if (mOrientation == HORIZONTAL) {
+ widthSpec = ViewGroup.getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), widthUsed, lp.width);
+ heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, heightUsed, lp.height);
+ } else {
+ heightSpec = ViewGroup.getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed, lp.height);
+ widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, widthUsed, lp.width);
+ }
+ child.measure(widthSpec, heightSpec);
+ if (DEBUG) {
+ Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec)
+ + " widthSpec " + Integer.toHexString(widthSpec)
+ + " heightSpec " + Integer.toHexString(heightSpec)
+ + " measuredWidth " + child.getMeasuredWidth()
+ + " measuredHeight " + child.getMeasuredHeight());
+ }
+ if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
+ if (TRACE) TraceCompat.endSection();
+ }
+
+ /**
+ * Get facet from the ViewHolder or the viewType.
+ */
+ <E> E getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass) {
+ E facet = null;
+ if (vh instanceof FacetProvider) {
+ facet = (E) ((FacetProvider) vh).getFacet(facetClass);
+ }
+ if (facet == null && mFacetProviderAdapter != null) {
+ FacetProvider p = mFacetProviderAdapter.getFacetProvider(vh.getItemViewType());
+ if (p != null) {
+ facet = (E) p.getFacet(facetClass);
+ }
+ }
+ return facet;
+ }
+
+ private Grid.Provider mGridProvider = new Grid.Provider() {
+
+ @Override
+ public int getMinIndex() {
+ return mPositionDeltaInPreLayout;
+ }
+
+ @Override
+ public int getCount() {
+ return mState.getItemCount() + mPositionDeltaInPreLayout;
+ }
+
+ @Override
+ public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) {
+ if (TRACE) TraceCompat.beginSection("createItem");
+ if (TRACE) TraceCompat.beginSection("getview");
+ View v = getViewForPosition(index - mPositionDeltaInPreLayout);
+ if (TRACE) TraceCompat.endSection();
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
+ lp.setItemAlignmentFacet((ItemAlignmentFacet)getFacet(vh, ItemAlignmentFacet.class));
+ // See recyclerView docs: we don't need re-add scraped view if it was removed.
+ if (!lp.isItemRemoved()) {
+ if (TRACE) TraceCompat.beginSection("addView");
+ if (disappearingItem) {
+ if (append) {
+ addDisappearingView(v);
+ } else {
+ addDisappearingView(v, 0);
+ }
+ } else {
+ if (append) {
+ addView(v);
+ } else {
+ addView(v, 0);
+ }
+ }
+ if (TRACE) TraceCompat.endSection();
+ if (mChildVisibility != -1) {
+ v.setVisibility(mChildVisibility);
+ }
+
+ if (mPendingMoveSmoothScroller != null) {
+ mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
+ }
+ int subindex = getSubPositionByView(v, v.findFocus());
+ if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
+ // when we are appending item during scroll pass and the item's position
+ // matches the mFocusPosition, we should signal a childSelected event.
+ // However if we are still running PendingMoveSmoothScroller, we defer and
+ // signal the event in PendingMoveSmoothScroller.onStop(). This can
+ // avoid lots of childSelected events during a long smooth scrolling and
+ // increase performance.
+ if (index == mFocusPosition && subindex == mSubFocusPosition
+ && mPendingMoveSmoothScroller == null) {
+ dispatchChildSelected();
+ }
+ } else if ((mFlag & PF_FAST_RELAYOUT) == 0) {
+ // fastRelayout will dispatch event at end of onLayoutChildren().
+ // For full layout, two situations here:
+ // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
+ // 2. mInLayoutSearchFocus is true: dispatchChildSelected() on first child
+ // equal to or after mFocusPosition that can take focus.
+ if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) == 0 && index == mFocusPosition
+ && subindex == mSubFocusPosition) {
+ dispatchChildSelected();
+ } else if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) != 0 && index >= mFocusPosition
+ && v.hasFocusable()) {
+ mFocusPosition = index;
+ mSubFocusPosition = subindex;
+ mFlag &= ~PF_IN_LAYOUT_SEARCH_FOCUS;
+ dispatchChildSelected();
+ }
+ }
+ measureChild(v);
+ }
+ item[0] = v;
+ return mOrientation == HORIZONTAL ? getDecoratedMeasuredWidthWithMargin(v)
+ : getDecoratedMeasuredHeightWithMargin(v);
+ }
+
+ @Override
+ public void addItem(Object item, int index, int length, int rowIndex, int edge) {
+ View v = (View) item;
+ int start, end;
+ if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
+ edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingMin()
+ : mWindowAlignment.mainAxis().getSize()
+ - mWindowAlignment.mainAxis().getPaddingMax();
+ }
+ boolean edgeIsMin = !mGrid.isReversedFlow();
+ if (edgeIsMin) {
+ start = edge;
+ end = edge + length;
+ } else {
+ start = edge - length;
+ end = edge;
+ }
+ int startSecondary = getRowStartSecondary(rowIndex)
+ + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
+ mChildrenStates.loadView(v, index);
+ layoutChild(rowIndex, v, start, end, startSecondary);
+ if (DEBUG) {
+ Log.d(getTag(), "addView " + index + " " + v);
+ }
+ if (TRACE) TraceCompat.endSection();
+
+ if (!mState.isPreLayout()) {
+ updateScrollLimits();
+ }
+ if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && mPendingMoveSmoothScroller != null) {
+ mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
+ }
+ if (mChildLaidOutListener != null) {
+ RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
+ mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
+ vh == null ? NO_ID : vh.getItemId());
+ }
+ }
+
+ @Override
+ public void removeItem(int index) {
+ if (TRACE) TraceCompat.beginSection("removeItem");
+ View v = findViewByPosition(index - mPositionDeltaInPreLayout);
+ if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
+ detachAndScrapView(v, mRecycler);
+ } else {
+ removeAndRecycleView(v, mRecycler);
+ }
+ if (TRACE) TraceCompat.endSection();
+ }
+
+ @Override
+ public int getEdge(int index) {
+ View v = findViewByPosition(index - mPositionDeltaInPreLayout);
+ return (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? getViewMax(v) : getViewMin(v);
+ }
+
+ @Override
+ public int getSize(int index) {
+ return getViewPrimarySize(findViewByPosition(index - mPositionDeltaInPreLayout));
+ }
+ };
+
+ void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
+ if (TRACE) TraceCompat.beginSection("layoutChild");
+ int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v)
+ : getDecoratedMeasuredWidthWithMargin(v);
+ if (mFixedRowSizeSecondary > 0) {
+ sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
+ }
+ final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int horizontalGravity = (mFlag & PF_REVERSE_FLOW_MASK) != 0
+ ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK,
+ View.LAYOUT_DIRECTION_RTL)
+ : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP)
+ || (mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT)) {
+ // do nothing
+ } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM)
+ || (mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT)) {
+ startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
+ } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL)
+ || (mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL)) {
+ startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
+ }
+ int left, top, right, bottom;
+ if (mOrientation == HORIZONTAL) {
+ left = start;
+ top = startSecondary;
+ right = end;
+ bottom = startSecondary + sizeSecondary;
+ } else {
+ top = start;
+ left = startSecondary;
+ bottom = end;
+ right = startSecondary + sizeSecondary;
+ }
+ LayoutParams params = (LayoutParams) v.getLayoutParams();
+ layoutDecoratedWithMargins(v, left, top, right, bottom);
+ // Now super.getDecoratedBoundsWithMargins() includes the extra space for optical bounds,
+ // subtracting it from value passed in layoutDecoratedWithMargins(), we can get the optical
+ // bounds insets.
+ super.getDecoratedBoundsWithMargins(v, sTempRect);
+ params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top,
+ sTempRect.right - right, sTempRect.bottom - bottom);
+ updateChildAlignments(v);
+ if (TRACE) TraceCompat.endSection();
+ }
+
+ private void updateChildAlignments(View v) {
+ final LayoutParams p = (LayoutParams) v.getLayoutParams();
+ if (p.getItemAlignmentFacet() == null) {
+ // Fallback to global settings on grid view
+ p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
+ p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
+ } else {
+ // Use ItemAlignmentFacet defined on specific ViewHolder
+ p.calculateItemAlignments(mOrientation, v);
+ if (mOrientation == HORIZONTAL) {
+ p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
+ } else {
+ p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
+ }
+ }
+ }
+
+ private void updateChildAlignments() {
+ for (int i = 0, c = getChildCount(); i < c; i++) {
+ updateChildAlignments(getChildAt(i));
+ }
+ }
+
+ void setExtraLayoutSpace(int extraLayoutSpace) {
+ if (mExtraLayoutSpace == extraLayoutSpace) {
+ return;
+ } else if (mExtraLayoutSpace < 0) {
+ throw new IllegalArgumentException("ExtraLayoutSpace must >= 0");
+ }
+ mExtraLayoutSpace = extraLayoutSpace;
+ requestLayout();
+ }
+
+ int getExtraLayoutSpace() {
+ return mExtraLayoutSpace;
+ }
+
+ private void removeInvisibleViewsAtEnd() {
+ if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
+ mGrid.removeInvisibleItemsAtEnd(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
+ }
+ }
+
+ private void removeInvisibleViewsAtFront() {
+ if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
+ mGrid.removeInvisibleItemsAtFront(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? mSizePrimary + mExtraLayoutSpace : -mExtraLayoutSpace);
+ }
+ }
+
+ private boolean appendOneColumnVisibleItems() {
+ return mGrid.appendOneColumnVisibleItems();
+ }
+
+ void slideIn() {
+ if ((mFlag & PF_SLIDING) != 0) {
+ mFlag &= ~PF_SLIDING;
+ if (mFocusPosition >= 0) {
+ scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+ } else {
+ mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
+ requestLayout();
+ }
+ if ((mFlag & PF_LAYOUT_EATEN_IN_SLIDING) != 0) {
+ mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
+ if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
+ mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (newState == SCROLL_STATE_IDLE) {
+ mBaseGridView.removeOnScrollListener(this);
+ requestLayout();
+ }
+ }
+ });
+ } else {
+ requestLayout();
+ }
+ }
+ }
+ }
+
+ int getSlideOutDistance() {
+ int distance;
+ if (mOrientation == VERTICAL) {
+ distance = -getHeight();
+ if (getChildCount() > 0) {
+ int top = getChildAt(0).getTop();
+ if (top < 0) {
+ // scroll more if first child is above top edge
+ distance = distance + top;
+ }
+ }
+ } else {
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
+ distance = getWidth();
+ if (getChildCount() > 0) {
+ int start = getChildAt(0).getRight();
+ if (start > distance) {
+ // scroll more if first child is outside right edge
+ distance = start;
+ }
+ }
+ } else {
+ distance = -getWidth();
+ if (getChildCount() > 0) {
+ int start = getChildAt(0).getLeft();
+ if (start < 0) {
+ // scroll more if first child is out side left edge
+ distance = distance + start;
+ }
+ }
+ }
+ }
+ return distance;
+ }
+
+ boolean isSlidingChildViews() {
+ return (mFlag & PF_SLIDING) != 0;
+ }
+
+ /**
+ * Temporarily slide out child and block layout and scroll requests.
+ */
+ void slideOut() {
+ if ((mFlag & PF_SLIDING) != 0) {
+ return;
+ }
+ mFlag |= PF_SLIDING;
+ if (getChildCount() == 0) {
+ return;
+ }
+ if (mOrientation == VERTICAL) {
+ mBaseGridView.smoothScrollBy(0, getSlideOutDistance(),
+ new AccelerateDecelerateInterpolator());
+ } else {
+ mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0,
+ new AccelerateDecelerateInterpolator());
+ }
+ }
+
+ private boolean prependOneColumnVisibleItems() {
+ return mGrid.prependOneColumnVisibleItems();
+ }
+
+ private void appendVisibleItems() {
+ mGrid.appendVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout
+ : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout);
+ }
+
+ private void prependVisibleItems() {
+ mGrid.prependVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout
+ : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout);
+ }
+
+ /**
+ * Fast layout when there is no structure change, adapter change, etc.
+ * It will layout all views was layout requested or updated, until hit a view
+ * with different size, then it break and detachAndScrap all views after that.
+ */
+ private void fastRelayout() {
+ boolean invalidateAfter = false;
+ final int childCount = getChildCount();
+ int position = mGrid.getFirstVisibleIndex();
+ int index = 0;
+ mFlag &= ~PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
+ for (; index < childCount; index++, position++) {
+ View view = getChildAt(index);
+ // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add
+ // extra views and invalidate existing Grid position. Also the prelayout calling
+ // getViewForPosotion() may retrieve item from cache with FLAG_INVALID. The adapter
+ // postion will be -1 for this case. Either case, we should invalidate after this item
+ // and call getViewForPosition() again to rebind.
+ if (position != getAdapterPositionByView(view)) {
+ invalidateAfter = true;
+ break;
+ }
+ Grid.Location location = mGrid.getLocation(position);
+ if (location == null) {
+ invalidateAfter = true;
+ break;
+ }
+
+ int startSecondary = getRowStartSecondary(location.row)
+ + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
+ int primarySize, end;
+ int start = getViewMin(view);
+ int oldPrimarySize = getViewPrimarySize(view);
+
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (lp.viewNeedsUpdate()) {
+ mFlag |= PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
+ detachAndScrapView(view, mRecycler);
+ view = getViewForPosition(position);
+ addView(view, index);
+ }
+
+ measureChild(view);
+ if (mOrientation == HORIZONTAL) {
+ primarySize = getDecoratedMeasuredWidthWithMargin(view);
+ end = start + primarySize;
+ } else {
+ primarySize = getDecoratedMeasuredHeightWithMargin(view);
+ end = start + primarySize;
+ }
+ layoutChild(location.row, view, start, end, startSecondary);
+ if (oldPrimarySize != primarySize) {
+ // size changed invalidate remaining Locations
+ if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
+ invalidateAfter = true;
+ break;
+ }
+ }
+ if (invalidateAfter) {
+ final int savedLastPos = mGrid.getLastVisibleIndex();
+ for (int i = childCount - 1; i >= index; i--) {
+ View v = getChildAt(i);
+ detachAndScrapView(v, mRecycler);
+ }
+ mGrid.invalidateItemsAfter(position);
+ if ((mFlag & PF_PRUNE_CHILD) != 0) {
+ // in regular prune child mode, we just append items up to edge limit
+ appendVisibleItems();
+ if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
+ // make sure add focus view back: the view might be outside edge limit
+ // when there is delta in onLayoutChildren().
+ while (mGrid.getLastVisibleIndex() < mFocusPosition) {
+ mGrid.appendOneColumnVisibleItems();
+ }
+ }
+ } else {
+ // prune disabled(e.g. in RowsFragment transition): append all removed items
+ while (mGrid.appendOneColumnVisibleItems()
+ && mGrid.getLastVisibleIndex() < savedLastPos);
+ }
+ }
+ updateScrollLimits();
+ updateSecondaryScrollLimits();
+ }
+
+ @Override
+ public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
+ if (TRACE) TraceCompat.beginSection("removeAndRecycleAllViews");
+ if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ if (TRACE) TraceCompat.endSection();
+ }
+
+ // called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable
+ // and scroll to the view if framework focus on it.
+ private void focusToViewInLayout(boolean hadFocus, boolean alignToView, int extraDelta,
+ int extraDeltaSecondary) {
+ View focusView = findViewByPosition(mFocusPosition);
+ if (focusView != null && alignToView) {
+ scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
+ }
+ if (focusView != null && hadFocus && !focusView.hasFocus()) {
+ focusView.requestFocus();
+ } else if (!hadFocus && !mBaseGridView.hasFocus()) {
+ if (focusView != null && focusView.hasFocusable()) {
+ mBaseGridView.focusableViewAvailable(focusView);
+ } else {
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ focusView = getChildAt(i);
+ if (focusView != null && focusView.hasFocusable()) {
+ mBaseGridView.focusableViewAvailable(focusView);
+ break;
+ }
+ }
+ }
+ // focusViewAvailable() might focus to the view, scroll to it if that is the case.
+ if (alignToView && focusView != null && focusView.hasFocus()) {
+ scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static class OnLayoutCompleteListener {
+ public void onLayoutCompleted(RecyclerView.State state) {
+ }
+ }
+
+ @VisibleForTesting
+ OnLayoutCompleteListener mLayoutCompleteListener;
+
+ @Override
+ public void onLayoutCompleted(State state) {
+ if (mLayoutCompleteListener != null) {
+ mLayoutCompleteListener.onLayoutCompleted(state);
+ }
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return true;
+ }
+
+ void updatePositionToRowMapInPostLayout() {
+ mPositionToRowInPostLayout.clear();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ // Grid still maps to old positions at this point, use old position to get row infor
+ int position = mBaseGridView.getChildViewHolder(getChildAt(i)).getOldPosition();
+ if (position >= 0) {
+ Grid.Location loc = mGrid.getLocation(position);
+ if (loc != null) {
+ mPositionToRowInPostLayout.put(position, loc.row);
+ }
+ }
+ }
+ }
+
+ void fillScrapViewsInPostLayout() {
+ List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList();
+ final int scrapSize = scrapList.size();
+ if (scrapSize == 0) {
+ return;
+ }
+ // initialize the int array or re-allocate the array.
+ if (mDisappearingPositions == null || scrapSize > mDisappearingPositions.length) {
+ int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length;
+ while (length < scrapSize) {
+ length = length << 1;
+ }
+ mDisappearingPositions = new int[length];
+ }
+ int totalItems = 0;
+ for (int i = 0; i < scrapSize; i++) {
+ int pos = scrapList.get(i).getAdapterPosition();
+ if (pos >= 0) {
+ mDisappearingPositions[totalItems++] = pos;
+ }
+ }
+ // totalItems now has the length of disappearing items
+ if (totalItems > 0) {
+ Arrays.sort(mDisappearingPositions, 0, totalItems);
+ mGrid.fillDisappearingItems(mDisappearingPositions, totalItems,
+ mPositionToRowInPostLayout);
+ }
+ mPositionToRowInPostLayout.clear();
+ }
+
+ // in prelayout, first child's getViewPosition can be smaller than old adapter position
+ // if there were items removed before first visible index. For example:
+ // visible items are 3, 4, 5, 6, deleting 1, 2, 3 from adapter; the view position in
+ // prelayout are not 3(deleted), 4, 5, 6. Instead it's 1(deleted), 2, 3, 4.
+ // So there is a delta (2 in this case) between last cached position and prelayout position.
+ void updatePositionDeltaInPreLayout() {
+ if (getChildCount() > 0) {
+ View view = getChildAt(0);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex()
+ - lp.getViewLayoutPosition();
+ } else {
+ mPositionDeltaInPreLayout = 0;
+ }
+ }
+
+ // Lays out items based on the current scroll position
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (DEBUG) {
+ Log.v(getTag(), "layoutChildren start numRows " + mNumRows
+ + " inPreLayout " + state.isPreLayout()
+ + " didStructureChange " + state.didStructureChange()
+ + " mForceFullLayout " + ((mFlag & PF_FORCE_FULL_LAYOUT) != 0));
+ Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
+ }
+
+ if (mNumRows == 0) {
+ // haven't done measure yet
+ return;
+ }
+ final int itemCount = state.getItemCount();
+ if (itemCount < 0) {
+ return;
+ }
+
+ if ((mFlag & PF_SLIDING) != 0) {
+ // if there is already children, delay the layout process until slideIn(), if it's
+ // first time layout children: scroll them offscreen at end of onLayoutChildren()
+ if (getChildCount() > 0) {
+ mFlag |= PF_LAYOUT_EATEN_IN_SLIDING;
+ return;
+ }
+ }
+ if ((mFlag & PF_LAYOUT_ENABLED) == 0) {
+ discardLayoutInfo();
+ removeAndRecycleAllViews(recycler);
+ return;
+ }
+ mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_LAYOUT;
+
+ saveContext(recycler, state);
+ if (state.isPreLayout()) {
+ updatePositionDeltaInPreLayout();
+ int childCount = getChildCount();
+ if (mGrid != null && childCount > 0) {
+ int minChangedEdge = Integer.MAX_VALUE;
+ int maxChangeEdge = Integer.MIN_VALUE;
+ int minOldAdapterPosition = mBaseGridView.getChildViewHolder(
+ getChildAt(0)).getOldPosition();
+ int maxOldAdapterPosition = mBaseGridView.getChildViewHolder(
+ getChildAt(childCount - 1)).getOldPosition();
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view);
+ // if either of following happening
+ // 1. item itself has changed or layout parameter changed
+ // 2. item is losing focus
+ // 3. item is gaining focus
+ // 4. item is moved out of old adapter position range.
+ if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested()
+ || (!view.hasFocus() && mFocusPosition == lp.getViewAdapterPosition())
+ || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition())
+ || newAdapterPosition < minOldAdapterPosition
+ || newAdapterPosition > maxOldAdapterPosition) {
+ minChangedEdge = Math.min(minChangedEdge, getViewMin(view));
+ maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view));
+ }
+ }
+ if (maxChangeEdge > minChangedEdge) {
+ mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge;
+ }
+ // append items for mExtraLayoutSpaceInPreLayout
+ appendVisibleItems();
+ prependVisibleItems();
+ }
+ mFlag &= ~PF_STAGE_MASK;
+ leaveContext();
+ if (DEBUG) Log.v(getTag(), "layoutChildren end");
+ return;
+ }
+
+ // save all view's row information before detach all views
+ if (state.willRunPredictiveAnimations()) {
+ updatePositionToRowMapInPostLayout();
+ }
+ // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling
+ final boolean scrollToFocus = !isSmoothScrolling()
+ && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
+ if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
+ mFocusPosition = mFocusPosition + mFocusPositionOffset;
+ mSubFocusPosition = 0;
+ }
+ mFocusPositionOffset = 0;
+
+ View savedFocusView = findViewByPosition(mFocusPosition);
+ int savedFocusPos = mFocusPosition;
+ int savedSubFocusPos = mSubFocusPosition;
+ boolean hadFocus = mBaseGridView.hasFocus();
+ final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION;
+ final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION;
+ final int deltaPrimary;
+ final int deltaSecondary;
+ if (mOrientation == HORIZONTAL) {
+ deltaPrimary = state.getRemainingScrollHorizontal();
+ deltaSecondary = state.getRemainingScrollVertical();
+ } else {
+ deltaSecondary = state.getRemainingScrollHorizontal();
+ deltaPrimary = state.getRemainingScrollVertical();
+ }
+ if (layoutInit()) {
+ mFlag |= PF_FAST_RELAYOUT;
+ // If grid view is empty, we will start from mFocusPosition
+ mGrid.setStart(mFocusPosition);
+ fastRelayout();
+ } else {
+ mFlag &= ~PF_FAST_RELAYOUT;
+ // layoutInit() has detached all views, so start from scratch
+ mFlag = (mFlag & ~PF_IN_LAYOUT_SEARCH_FOCUS)
+ | (hadFocus ? PF_IN_LAYOUT_SEARCH_FOCUS : 0);
+ int startFromPosition, endPos;
+ if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex
+ || mFocusPosition < firstVisibleIndex)) {
+ startFromPosition = endPos = mFocusPosition;
+ } else {
+ startFromPosition = firstVisibleIndex;
+ endPos = lastVisibleIndex;
+ }
+ mGrid.setStart(startFromPosition);
+ if (endPos != NO_POSITION) {
+ while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) {
+ // continuously append items until endPos
+ }
+ }
+ }
+ // multiple rounds: scrollToView of first round may drag first/last child into
+ // "visible window" and we update scrollMin/scrollMax then run second scrollToView
+ // we must do this for fastRelayout() for the append item case
+ int oldFirstVisible;
+ int oldLastVisible;
+ do {
+ updateScrollLimits();
+ oldFirstVisible = mGrid.getFirstVisibleIndex();
+ oldLastVisible = mGrid.getLastVisibleIndex();
+ focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary);
+ appendVisibleItems();
+ prependVisibleItems();
+ // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise
+ // loop may bounce between scroll forward and scroll backward forever. Example:
+ // Assuming there are 19 items, child#18 and child#19 are both in RV, we are
+ // trying to focus to child#18 and there are 200px remaining scroll distance.
+ // 1 focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on
+ // right edge, but there to compensate remaining scroll 200px, also scroll
+ // backward 200px, 150px pushes last child#19 out side of right edge.
+ // 2 removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits()
+ // invalidates scroll max
+ // 3 In next iteration, when scroll max/min is unknown, focusToViewInLayout() will
+ // align focused child#18 at center of screen.
+ // 4 Because #18 is aligned at center, appendVisibleItems() will fill child#19 to
+ // the right.
+ // 5 (back to 1 and loop forever)
+ } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
+ || mGrid.getLastVisibleIndex() != oldLastVisible);
+ removeInvisibleViewsAtFront();
+ removeInvisibleViewsAtEnd();
+
+ if (state.willRunPredictiveAnimations()) {
+ fillScrapViewsInPostLayout();
+ }
+
+ if (DEBUG) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ mGrid.debugPrint(pw);
+ Log.d(getTag(), sw.toString());
+ }
+
+ if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
+ mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
+ } else {
+ updateRowSecondarySizeRefresh();
+ }
+
+ // For fastRelayout, only dispatch event when focus position changes or selected item
+ // being updated.
+ if ((mFlag & PF_FAST_RELAYOUT) != 0 && (mFocusPosition != savedFocusPos || mSubFocusPosition
+ != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView
+ || (mFlag & PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION) != 0)) {
+ dispatchChildSelected();
+ } else if ((mFlag & (PF_FAST_RELAYOUT | PF_IN_LAYOUT_SEARCH_FOCUS))
+ == PF_IN_LAYOUT_SEARCH_FOCUS) {
+ // For full layout we dispatchChildSelected() in createItem() unless searched all
+ // children and found none is focusable then dispatchChildSelected() here.
+ dispatchChildSelected();
+ }
+ dispatchChildSelectedAndPositioned();
+ if ((mFlag & PF_SLIDING) != 0) {
+ scrollDirectionPrimary(getSlideOutDistance());
+ }
+
+ mFlag &= ~PF_STAGE_MASK;
+ leaveContext();
+ if (DEBUG) Log.v(getTag(), "layoutChildren end");
+ }
+
+ private void offsetChildrenSecondary(int increment) {
+ final int childCount = getChildCount();
+ if (mOrientation == HORIZONTAL) {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetTopAndBottom(increment);
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetLeftAndRight(increment);
+ }
+ }
+ }
+
+ private void offsetChildrenPrimary(int increment) {
+ final int childCount = getChildCount();
+ if (mOrientation == VERTICAL) {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetTopAndBottom(increment);
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetLeftAndRight(increment);
+ }
+ }
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
+ if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
+ if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
+ return 0;
+ }
+ saveContext(recycler, state);
+ mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
+ int result;
+ if (mOrientation == HORIZONTAL) {
+ result = scrollDirectionPrimary(dx);
+ } else {
+ result = scrollDirectionSecondary(dx);
+ }
+ leaveContext();
+ mFlag &= ~PF_STAGE_MASK;
+ return result;
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
+ if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
+ if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
+ return 0;
+ }
+ mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
+ saveContext(recycler, state);
+ int result;
+ if (mOrientation == VERTICAL) {
+ result = scrollDirectionPrimary(dy);
+ } else {
+ result = scrollDirectionSecondary(dy);
+ }
+ leaveContext();
+ mFlag &= ~PF_STAGE_MASK;
+ return result;
+ }
+
+ // scroll in main direction may add/prune views
+ private int scrollDirectionPrimary(int da) {
+ if (TRACE) TraceCompat.beginSection("scrollPrimary");
+ // We apply the cap of maxScroll/minScroll to the delta, except for two cases:
+ // 1. when children are in sliding out mode
+ // 2. During onLayoutChildren(), it may compensate the remaining scroll delta,
+ // we should honor the request regardless if it goes over minScroll / maxScroll.
+ // (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1)
+ if ((mFlag & PF_SLIDING) == 0 && (mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
+ if (da > 0) {
+ if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
+ int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
+ if (da > maxScroll) {
+ da = maxScroll;
+ }
+ }
+ } else if (da < 0) {
+ if (!mWindowAlignment.mainAxis().isMinUnknown()) {
+ int minScroll = mWindowAlignment.mainAxis().getMinScroll();
+ if (da < minScroll) {
+ da = minScroll;
+ }
+ }
+ }
+ }
+ if (da == 0) {
+ if (TRACE) TraceCompat.endSection();
+ return 0;
+ }
+ offsetChildrenPrimary(-da);
+ if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
+ updateScrollLimits();
+ if (TRACE) TraceCompat.endSection();
+ return da;
+ }
+
+ int childCount = getChildCount();
+ boolean updated;
+
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
+ prependVisibleItems();
+ } else {
+ appendVisibleItems();
+ }
+ updated = getChildCount() > childCount;
+ childCount = getChildCount();
+
+ if (TRACE) TraceCompat.beginSection("remove");
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
+ removeInvisibleViewsAtEnd();
+ } else {
+ removeInvisibleViewsAtFront();
+ }
+ if (TRACE) TraceCompat.endSection();
+ updated |= getChildCount() < childCount;
+ if (updated) {
+ updateRowSecondarySizeRefresh();
+ }
+
+ mBaseGridView.invalidate();
+ updateScrollLimits();
+ if (TRACE) TraceCompat.endSection();
+ return da;
+ }
+
+ // scroll in second direction will not add/prune views
+ private int scrollDirectionSecondary(int dy) {
+ if (dy == 0) {
+ return 0;
+ }
+ offsetChildrenSecondary(-dy);
+ mScrollOffsetSecondary += dy;
+ updateSecondaryScrollLimits();
+ mBaseGridView.invalidate();
+ return dy;
+ }
+
+ @Override
+ public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ try {
+ saveContext(null, state);
+ int da = (mOrientation == HORIZONTAL) ? dx : dy;
+ if (getChildCount() == 0 || da == 0) {
+ // can't support this scroll, so don't bother prefetching
+ return;
+ }
+
+ int fromLimit = da < 0
+ ? -mExtraLayoutSpace
+ : mSizePrimary + mExtraLayoutSpace;
+ mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry);
+ } finally {
+ leaveContext();
+ }
+ }
+
+ @Override
+ public void collectInitialPrefetchPositions(int adapterItemCount,
+ LayoutPrefetchRegistry layoutPrefetchRegistry) {
+ int numToPrefetch = mBaseGridView.mInitialPrefetchItemCount;
+ if (adapterItemCount != 0 && numToPrefetch != 0) {
+ // prefetch items centered around mFocusPosition
+ int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1)/ 2,
+ adapterItemCount - numToPrefetch));
+ for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) {
+ layoutPrefetchRegistry.addPosition(i, 0);
+ }
+ }
+ }
+
+ void updateScrollLimits() {
+ if (mState.getItemCount() == 0) {
+ return;
+ }
+ int highVisiblePos, lowVisiblePos;
+ int highMaxPos, lowMinPos;
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) == 0) {
+ highVisiblePos = mGrid.getLastVisibleIndex();
+ highMaxPos = mState.getItemCount() - 1;
+ lowVisiblePos = mGrid.getFirstVisibleIndex();
+ lowMinPos = 0;
+ } else {
+ highVisiblePos = mGrid.getFirstVisibleIndex();
+ highMaxPos = 0;
+ lowVisiblePos = mGrid.getLastVisibleIndex();
+ lowMinPos = mState.getItemCount() - 1;
+ }
+ if (highVisiblePos < 0 || lowVisiblePos < 0) {
+ return;
+ }
+ final boolean highAvailable = highVisiblePos == highMaxPos;
+ final boolean lowAvailable = lowVisiblePos == lowMinPos;
+ if (!highAvailable && mWindowAlignment.mainAxis().isMaxUnknown()
+ && !lowAvailable && mWindowAlignment.mainAxis().isMinUnknown()) {
+ return;
+ }
+ int maxEdge, maxViewCenter;
+ if (highAvailable) {
+ maxEdge = mGrid.findRowMax(true, sTwoInts);
+ View maxChild = findViewByPosition(sTwoInts[1]);
+ maxViewCenter = getViewCenter(maxChild);
+ final LayoutParams lp = (LayoutParams) maxChild.getLayoutParams();
+ int[] multipleAligns = lp.getAlignMultiple();
+ if (multipleAligns != null && multipleAligns.length > 0) {
+ maxViewCenter += multipleAligns[multipleAligns.length - 1] - multipleAligns[0];
+ }
+ } else {
+ maxEdge = Integer.MAX_VALUE;
+ maxViewCenter = Integer.MAX_VALUE;
+ }
+ int minEdge, minViewCenter;
+ if (lowAvailable) {
+ minEdge = mGrid.findRowMin(false, sTwoInts);
+ View minChild = findViewByPosition(sTwoInts[1]);
+ minViewCenter = getViewCenter(minChild);
+ } else {
+ minEdge = Integer.MIN_VALUE;
+ minViewCenter = Integer.MIN_VALUE;
+ }
+ mWindowAlignment.mainAxis().updateMinMax(minEdge, maxEdge, minViewCenter, maxViewCenter);
+ }
+
+ /**
+ * Update secondary axis's scroll min/max, should be updated in
+ * {@link #scrollDirectionSecondary(int)}.
+ */
+ private void updateSecondaryScrollLimits() {
+ WindowAlignment.Axis secondAxis = mWindowAlignment.secondAxis();
+ int minEdge = secondAxis.getPaddingMin() - mScrollOffsetSecondary;
+ int maxEdge = minEdge + getSizeSecondary();
+ secondAxis.updateMinMax(minEdge, maxEdge, minEdge, maxEdge);
+ }
+
+ private void initScrollController() {
+ mWindowAlignment.reset();
+ mWindowAlignment.horizontal.setSize(getWidth());
+ mWindowAlignment.vertical.setSize(getHeight());
+ mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
+ mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
+ mSizePrimary = mWindowAlignment.mainAxis().getSize();
+ mScrollOffsetSecondary = 0;
+
+ if (DEBUG) {
+ Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
+ + " mWindowAlignment " + mWindowAlignment);
+ }
+ }
+
+ private void updateScrollController() {
+ mWindowAlignment.horizontal.setSize(getWidth());
+ mWindowAlignment.vertical.setSize(getHeight());
+ mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
+ mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
+ mSizePrimary = mWindowAlignment.mainAxis().getSize();
+
+ if (DEBUG) {
+ Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
+ + " mWindowAlignment " + mWindowAlignment);
+ }
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ setSelection(position, 0, false, 0);
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+ int position) {
+ setSelection(position, 0, true, 0);
+ }
+
+ public void setSelection(int position,
+ int primaryScrollExtra) {
+ setSelection(position, 0, false, primaryScrollExtra);
+ }
+
+ public void setSelectionSmooth(int position) {
+ setSelection(position, 0, true, 0);
+ }
+
+ public void setSelectionWithSub(int position, int subposition,
+ int primaryScrollExtra) {
+ setSelection(position, subposition, false, primaryScrollExtra);
+ }
+
+ public void setSelectionSmoothWithSub(int position, int subposition) {
+ setSelection(position, subposition, true, 0);
+ }
+
+ public int getSelection() {
+ return mFocusPosition;
+ }
+
+ public int getSubSelection() {
+ return mSubFocusPosition;
+ }
+
+ public void setSelection(int position, int subposition, boolean smooth,
+ int primaryScrollExtra) {
+ if ((mFocusPosition != position && position != NO_POSITION)
+ || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
+ scrollToSelection(position, subposition, smooth, primaryScrollExtra);
+ }
+ }
+
+ void scrollToSelection(int position, int subposition,
+ boolean smooth, int primaryScrollExtra) {
+ if (TRACE) TraceCompat.beginSection("scrollToSelection");
+ mPrimaryScrollExtra = primaryScrollExtra;
+ View view = findViewByPosition(position);
+ // scrollToView() is based on Adapter position. Only call scrollToView() when item
+ // is still valid.
+ if (view != null && getAdapterPositionByView(view) == position) {
+ mFlag |= PF_IN_SELECTION;
+ scrollToView(view, smooth);
+ mFlag &= ~PF_IN_SELECTION;
+ } else {
+ mFocusPosition = position;
+ mSubFocusPosition = subposition;
+ mFocusPositionOffset = Integer.MIN_VALUE;
+ if ((mFlag & PF_LAYOUT_ENABLED) == 0 || (mFlag & PF_SLIDING) != 0) {
+ return;
+ }
+ if (smooth) {
+ if (!hasDoneFirstLayout()) {
+ Log.w(getTag(), "setSelectionSmooth should "
+ + "not be called before first layout pass");
+ return;
+ }
+ position = startPositionSmoothScroller(position);
+ if (position != mFocusPosition) {
+ // gets cropped by adapter size
+ mFocusPosition = position;
+ mSubFocusPosition = 0;
+ }
+ } else {
+ mFlag |= PF_FORCE_FULL_LAYOUT;
+ requestLayout();
+ }
+ }
+ if (TRACE) TraceCompat.endSection();
+ }
+
+ int startPositionSmoothScroller(int position) {
+ LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(0));
+ // TODO We should be able to deduce direction from bounds of current and target
+ // focus, rather than making assumptions about positions and directionality
+ final boolean isStart = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+ ? targetPosition > firstChildPos
+ : targetPosition < firstChildPos;
+ final int direction = isStart ? -1 : 1;
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+
+ };
+ linearSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(linearSmoothScroller);
+ return linearSmoothScroller.getTargetPosition();
+ }
+
+ private void processPendingMovement(boolean forward) {
+ if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
+ return;
+ }
+ if (mPendingMoveSmoothScroller == null) {
+ // Stop existing scroller and create a new PendingMoveSmoothScroller.
+ mBaseGridView.stopScroll();
+ PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
+ forward ? 1 : -1, mNumRows > 1);
+ mFocusPositionOffset = 0;
+ startSmoothScroll(linearSmoothScroller);
+ if (linearSmoothScroller.isRunning()) {
+ mPendingMoveSmoothScroller = linearSmoothScroller;
+ }
+ } else {
+ if (forward) {
+ mPendingMoveSmoothScroller.increasePendingMoves();
+ } else {
+ mPendingMoveSmoothScroller.decreasePendingMoves();
+ }
+ }
+ }
+
+ // Observer is registered on Adapter to invalidate saved instance state
+ final RecyclerView.AdapterDataObserver mObServer = new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ mChildrenStates.clear();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ if (DEBUG) {
+ Log.v(getTag(), "onItemRangeChanged positionStart "
+ + positionStart + " itemCount " + itemCount);
+ }
+ for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
+ mChildrenStates.remove(i);
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ mChildrenStates.clear();
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ mChildrenStates.clear();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ mChildrenStates.clear();
+ }
+ };
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
+ + positionStart + " itemCount " + itemCount);
+ if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
+ && mFocusPositionOffset != Integer.MIN_VALUE) {
+ int pos = mFocusPosition + mFocusPositionOffset;
+ if (positionStart <= pos) {
+ mFocusPositionOffset += itemCount;
+ }
+ }
+ }
+
+ @Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ if (DEBUG) Log.v(getTag(), "onItemsChanged");
+ mFocusPositionOffset = 0;
+ }
+
+ @Override
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
+ + positionStart + " itemCount " + itemCount);
+ if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
+ && mFocusPositionOffset != Integer.MIN_VALUE) {
+ int pos = mFocusPosition + mFocusPositionOffset;
+ if (positionStart <= pos) {
+ if (positionStart + itemCount > pos) {
+ // stop updating offset after the focus item was removed
+ mFocusPositionOffset += positionStart - pos;
+ mFocusPosition += mFocusPositionOffset;
+ mFocusPositionOffset = Integer.MIN_VALUE;
+ } else {
+ mFocusPositionOffset -= itemCount;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
+ int itemCount) {
+ if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
+ + fromPosition + " toPosition " + toPosition);
+ if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
+ int pos = mFocusPosition + mFocusPositionOffset;
+ if (fromPosition <= pos && pos < fromPosition + itemCount) {
+ // moved items include focused position
+ mFocusPositionOffset += toPosition - fromPosition;
+ } else if (fromPosition < pos && toPosition > pos - itemCount) {
+ // move items before focus position to after focused position
+ mFocusPositionOffset -= itemCount;
+ } else if (fromPosition > pos && toPosition < pos) {
+ // move items after focus position to before focused position
+ mFocusPositionOffset += itemCount;
+ }
+ }
+ }
+
+ @Override
+ public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
+ if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
+ return true;
+ }
+ if (getAdapterPositionByView(child) == NO_POSITION) {
+ // This is could be the last view in DISAPPEARING animation.
+ return true;
+ }
+ if ((mFlag & (PF_STAGE_MASK | PF_IN_SELECTION)) == 0) {
+ scrollToView(child, focused, true);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
+ boolean immediate) {
+ if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
+ return false;
+ }
+
+ public void getViewSelectedOffsets(View view, int[] offsets) {
+ if (mOrientation == HORIZONTAL) {
+ offsets[0] = getPrimaryAlignedScrollDistance(view);
+ offsets[1] = getSecondaryScrollDistance(view);
+ } else {
+ offsets[1] = getPrimaryAlignedScrollDistance(view);
+ offsets[0] = getSecondaryScrollDistance(view);
+ }
+ }
+
+ /**
+ * Return the scroll delta on primary direction to make the view selected. If the return value
+ * is 0, there is no need to scroll.
+ */
+ private int getPrimaryAlignedScrollDistance(View view) {
+ return mWindowAlignment.mainAxis().getScroll(getViewCenter(view));
+ }
+
+ /**
+ * Get adjusted primary position for a given childView (if there is multiple ItemAlignment
+ * defined on the view).
+ */
+ private int getAdjustedPrimaryAlignedScrollDistance(int scrollPrimary, View view,
+ View childView) {
+ int subindex = getSubPositionByView(view, childView);
+ if (subindex != 0) {
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0];
+ }
+ return scrollPrimary;
+ }
+
+ private int getSecondaryScrollDistance(View view) {
+ int viewCenterSecondary = getViewCenterSecondary(view);
+ return mWindowAlignment.secondAxis().getScroll(viewCenterSecondary);
+ }
+
+ /**
+ * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
+ */
+ void scrollToView(View view, boolean smooth) {
+ scrollToView(view, view == null ? null : view.findFocus(), smooth);
+ }
+
+ void scrollToView(View view, boolean smooth, int extraDelta, int extraDeltaSecondary) {
+ scrollToView(view, view == null ? null : view.findFocus(), smooth, extraDelta,
+ extraDeltaSecondary);
+ }
+
+ private void scrollToView(View view, View childView, boolean smooth) {
+ scrollToView(view, childView, smooth, 0, 0);
+ }
+ /**
+ * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
+ */
+ private void scrollToView(View view, View childView, boolean smooth, int extraDelta,
+ int extraDeltaSecondary) {
+ if ((mFlag & PF_SLIDING) != 0) {
+ return;
+ }
+ int newFocusPosition = getAdapterPositionByView(view);
+ int newSubFocusPosition = getSubPositionByView(view, childView);
+ if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
+ mFocusPosition = newFocusPosition;
+ mSubFocusPosition = newSubFocusPosition;
+ mFocusPositionOffset = 0;
+ if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
+ dispatchChildSelected();
+ }
+ if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
+ mBaseGridView.invalidate();
+ }
+ }
+ if (view == null) {
+ return;
+ }
+ if (!view.hasFocus() && mBaseGridView.hasFocus()) {
+ // transfer focus to the child if it does not have focus yet (e.g. triggered
+ // by setSelection())
+ view.requestFocus();
+ }
+ if ((mFlag & PF_SCROLL_ENABLED) == 0 && smooth) {
+ return;
+ }
+ if (getScrollPosition(view, childView, sTwoInts)
+ || extraDelta != 0 || extraDeltaSecondary != 0) {
+ scrollGrid(sTwoInts[0] + extraDelta, sTwoInts[1] + extraDeltaSecondary, smooth);
+ }
+ }
+
+ boolean getScrollPosition(View view, View childView, int[] deltas) {
+ switch (mFocusScrollStrategy) {
+ case BaseGridView.FOCUS_SCROLL_ALIGNED:
+ default:
+ return getAlignedPosition(view, childView, deltas);
+ case BaseGridView.FOCUS_SCROLL_ITEM:
+ case BaseGridView.FOCUS_SCROLL_PAGE:
+ return getNoneAlignedPosition(view, deltas);
+ }
+ }
+
+ private boolean getNoneAlignedPosition(View view, int[] deltas) {
+ int pos = getAdapterPositionByView(view);
+ int viewMin = getViewMin(view);
+ int viewMax = getViewMax(view);
+ // we either align "firstView" to left/top padding edge
+ // or align "lastView" to right/bottom padding edge
+ View firstView = null;
+ View lastView = null;
+ int paddingMin = mWindowAlignment.mainAxis().getPaddingMin();
+ int clientSize = mWindowAlignment.mainAxis().getClientSize();
+ final int row = mGrid.getRowIndex(pos);
+ if (viewMin < paddingMin) {
+ // view enters low padding area:
+ firstView = view;
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
+ // scroll one "page" left/top,
+ // align first visible item of the "page" at the low padding edge.
+ while (prependOneColumnVisibleItems()) {
+ CircularIntArray positions =
+ mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
+ firstView = findViewByPosition(positions.get(0));
+ if (viewMax - getViewMin(firstView) > clientSize) {
+ if (positions.size() > 2) {
+ firstView = findViewByPosition(positions.get(2));
+ }
+ break;
+ }
+ }
+ }
+ } else if (viewMax > clientSize + paddingMin) {
+ // view enters high padding area:
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
+ // scroll whole one page right/bottom, align view at the low padding edge.
+ firstView = view;
+ do {
+ CircularIntArray positions =
+ mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
+ lastView = findViewByPosition(positions.get(positions.size() - 1));
+ if (getViewMax(lastView) - viewMin > clientSize) {
+ lastView = null;
+ break;
+ }
+ } while (appendOneColumnVisibleItems());
+ if (lastView != null) {
+ // however if we reached end, we should align last view.
+ firstView = null;
+ }
+ } else {
+ lastView = view;
+ }
+ }
+ int scrollPrimary = 0;
+ int scrollSecondary = 0;
+ if (firstView != null) {
+ scrollPrimary = getViewMin(firstView) - paddingMin;
+ } else if (lastView != null) {
+ scrollPrimary = getViewMax(lastView) - (paddingMin + clientSize);
+ }
+ View secondaryAlignedView;
+ if (firstView != null) {
+ secondaryAlignedView = firstView;
+ } else if (lastView != null) {
+ secondaryAlignedView = lastView;
+ } else {
+ secondaryAlignedView = view;
+ }
+ scrollSecondary = getSecondaryScrollDistance(secondaryAlignedView);
+ if (scrollPrimary != 0 || scrollSecondary != 0) {
+ deltas[0] = scrollPrimary;
+ deltas[1] = scrollSecondary;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean getAlignedPosition(View view, View childView, int[] deltas) {
+ int scrollPrimary = getPrimaryAlignedScrollDistance(view);
+ if (childView != null) {
+ scrollPrimary = getAdjustedPrimaryAlignedScrollDistance(scrollPrimary, view, childView);
+ }
+ int scrollSecondary = getSecondaryScrollDistance(view);
+ if (DEBUG) {
+ Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
+ + " " + mPrimaryScrollExtra + " " + mWindowAlignment);
+ }
+ scrollPrimary += mPrimaryScrollExtra;
+ if (scrollPrimary != 0 || scrollSecondary != 0) {
+ deltas[0] = scrollPrimary;
+ deltas[1] = scrollSecondary;
+ return true;
+ } else {
+ deltas[0] = 0;
+ deltas[1] = 0;
+ }
+ return false;
+ }
+
+ private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
+ if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
+ scrollDirectionPrimary(scrollPrimary);
+ scrollDirectionSecondary(scrollSecondary);
+ } else {
+ int scrollX;
+ int scrollY;
+ if (mOrientation == HORIZONTAL) {
+ scrollX = scrollPrimary;
+ scrollY = scrollSecondary;
+ } else {
+ scrollX = scrollSecondary;
+ scrollY = scrollPrimary;
+ }
+ if (smooth) {
+ mBaseGridView.smoothScrollBy(scrollX, scrollY);
+ } else {
+ mBaseGridView.scrollBy(scrollX, scrollY);
+ dispatchChildSelectedAndPositioned();
+ }
+ }
+ }
+
+ public void setPruneChild(boolean pruneChild) {
+ if (((mFlag & PF_PRUNE_CHILD) != 0) != pruneChild) {
+ mFlag = (mFlag & ~PF_PRUNE_CHILD) | (pruneChild ? PF_PRUNE_CHILD : 0);
+ if (pruneChild) {
+ requestLayout();
+ }
+ }
+ }
+
+ public boolean getPruneChild() {
+ return (mFlag & PF_PRUNE_CHILD) != 0;
+ }
+
+ public void setScrollEnabled(boolean scrollEnabled) {
+ if (((mFlag & PF_SCROLL_ENABLED) != 0) != scrollEnabled) {
+ mFlag = (mFlag & ~PF_SCROLL_ENABLED) | (scrollEnabled ? PF_SCROLL_ENABLED : 0);
+ if (((mFlag & PF_SCROLL_ENABLED) != 0)
+ && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
+ && mFocusPosition != NO_POSITION) {
+ scrollToSelection(mFocusPosition, mSubFocusPosition,
+ true, mPrimaryScrollExtra);
+ }
+ }
+ }
+
+ public boolean isScrollEnabled() {
+ return (mFlag & PF_SCROLL_ENABLED) != 0;
+ }
+
+ private int findImmediateChildIndex(View view) {
+ if (mBaseGridView != null && view != mBaseGridView) {
+ view = findContainingItemView(view);
+ if (view != null) {
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ if (getChildAt(i) == view) {
+ return i;
+ }
+ }
+ }
+ }
+ return NO_POSITION;
+ }
+
+ void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ if (gainFocus) {
+ // if gridview.requestFocus() is called, select first focusable child.
+ for (int i = mFocusPosition; ;i++) {
+ View view = findViewByPosition(i);
+ if (view == null) {
+ break;
+ }
+ if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) {
+ view.requestFocus();
+ break;
+ }
+ }
+ }
+ }
+
+ void setFocusSearchDisabled(boolean disabled) {
+ mFlag = (mFlag & ~PF_FOCUS_SEARCH_DISABLED) | (disabled ? PF_FOCUS_SEARCH_DISABLED : 0);
+ }
+
+ boolean isFocusSearchDisabled() {
+ return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0;
+ }
+
+ @Override
+ public View onInterceptFocusSearch(View focused, int direction) {
+ if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
+ return focused;
+ }
+
+ final FocusFinder ff = FocusFinder.getInstance();
+ View result = null;
+ if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
+ // convert direction to absolute direction and see if we have a view there and if not
+ // tell LayoutManager to add if it can.
+ if (canScrollVertically()) {
+ final int absDir =
+ direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
+ result = ff.findNextFocus(mBaseGridView, focused, absDir);
+ }
+ if (canScrollHorizontally()) {
+ boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
+ final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
+ ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ result = ff.findNextFocus(mBaseGridView, focused, absDir);
+ }
+ } else {
+ result = ff.findNextFocus(mBaseGridView, focused, direction);
+ }
+ if (result != null) {
+ return result;
+ }
+
+ if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
+ return mBaseGridView.getParent().focusSearch(focused, direction);
+ }
+
+ if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction);
+ int movement = getMovement(direction);
+ final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
+ if (movement == NEXT_ITEM) {
+ if (isScroll || (mFlag & PF_FOCUS_OUT_END) == 0) {
+ result = focused;
+ }
+ if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedLastItem()) {
+ processPendingMovement(true);
+ result = focused;
+ }
+ } else if (movement == PREV_ITEM) {
+ if (isScroll || (mFlag & PF_FOCUS_OUT_FRONT) == 0) {
+ result = focused;
+ }
+ if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedFirstItem()) {
+ processPendingMovement(false);
+ result = focused;
+ }
+ } else if (movement == NEXT_ROW) {
+ if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_END) == 0) {
+ result = focused;
+ }
+ } else if (movement == PREV_ROW) {
+ if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_START) == 0) {
+ result = focused;
+ }
+ }
+ if (result != null) {
+ return result;
+ }
+
+ if (DEBUG) Log.v(getTag(), "now focusSearch in parent");
+ result = mBaseGridView.getParent().focusSearch(focused, direction);
+ if (result != null) {
+ return result;
+ }
+ return focused != null ? focused : mBaseGridView;
+ }
+
+ boolean hasPreviousViewInSameRow(int pos) {
+ if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
+ return false;
+ }
+ if (mGrid.getFirstVisibleIndex() > 0) {
+ return true;
+ }
+ final int focusedRow = mGrid.getLocation(pos).row;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ int position = getAdapterPositionByIndex(i);
+ Grid.Location loc = mGrid.getLocation(position);
+ if (loc != null && loc.row == focusedRow) {
+ if (position < pos) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onAddFocusables(RecyclerView recyclerView,
+ ArrayList<View> views, int direction, int focusableMode) {
+ if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
+ return true;
+ }
+ // If this viewgroup or one of its children currently has focus then we
+ // consider our children for focus searching in main direction on the same row.
+ // If this viewgroup has no focus and using focus align, we want the system
+ // to ignore our children and pass focus to the viewgroup, which will pass
+ // focus on to its children appropriately.
+ // If this viewgroup has no focus and not using focus align, we want to
+ // consider the child that does not overlap with padding area.
+ if (recyclerView.hasFocus()) {
+ if (mPendingMoveSmoothScroller != null) {
+ // don't find next focusable if has pending movement.
+ return true;
+ }
+ final int movement = getMovement(direction);
+ final View focused = recyclerView.findFocus();
+ final int focusedIndex = findImmediateChildIndex(focused);
+ final int focusedPos = getAdapterPositionByIndex(focusedIndex);
+ // Even if focusedPos != NO_POSITION, findViewByPosition could return null if the view
+ // is ignored or getLayoutPosition does not match the adapter position of focused view.
+ final View immediateFocusedChild = (focusedPos == NO_POSITION) ? null
+ : findViewByPosition(focusedPos);
+ // Add focusables of focused item.
+ if (immediateFocusedChild != null) {
+ immediateFocusedChild.addFocusables(views, direction, focusableMode);
+ }
+ if (mGrid == null || getChildCount() == 0) {
+ // no grid information, or no child, bail out.
+ return true;
+ }
+ if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) {
+ // For single row, cannot navigate to previous/next row.
+ return true;
+ }
+ // Add focusables of neighbor depending on the focus search direction.
+ final int focusedRow = mGrid != null && immediateFocusedChild != null
+ ? mGrid.getLocation(focusedPos).row : NO_POSITION;
+ final int focusableCount = views.size();
+ int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1;
+ int loop_end = inc > 0 ? getChildCount() - 1 : 0;
+ int loop_start;
+ if (focusedIndex == NO_POSITION) {
+ loop_start = inc > 0 ? 0 : getChildCount() - 1;
+ } else {
+ loop_start = focusedIndex + inc;
+ }
+ for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) {
+ continue;
+ }
+ // if there wasn't any focused item, add the very first focusable
+ // items and stop.
+ if (immediateFocusedChild == null) {
+ child.addFocusables(views, direction, focusableMode);
+ if (views.size() > focusableCount) {
+ break;
+ }
+ continue;
+ }
+ int position = getAdapterPositionByIndex(i);
+ Grid.Location loc = mGrid.getLocation(position);
+ if (loc == null) {
+ continue;
+ }
+ if (movement == NEXT_ITEM) {
+ // Add first focusable item on the same row
+ if (loc.row == focusedRow && position > focusedPos) {
+ child.addFocusables(views, direction, focusableMode);
+ if (views.size() > focusableCount) {
+ break;
+ }
+ }
+ } else if (movement == PREV_ITEM) {
+ // Add first focusable item on the same row
+ if (loc.row == focusedRow && position < focusedPos) {
+ child.addFocusables(views, direction, focusableMode);
+ if (views.size() > focusableCount) {
+ break;
+ }
+ }
+ } else if (movement == NEXT_ROW) {
+ // Add all focusable items after this item whose row index is bigger
+ if (loc.row == focusedRow) {
+ continue;
+ } else if (loc.row < focusedRow) {
+ break;
+ }
+ child.addFocusables(views, direction, focusableMode);
+ } else if (movement == PREV_ROW) {
+ // Add all focusable items before this item whose row index is smaller
+ if (loc.row == focusedRow) {
+ continue;
+ } else if (loc.row > focusedRow) {
+ break;
+ }
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ } else {
+ int focusableCount = views.size();
+ if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
+ // adding views not overlapping padding area to avoid scrolling in gaining focus
+ int left = mWindowAlignment.mainAxis().getPaddingMin();
+ int right = mWindowAlignment.mainAxis().getClientSize() + left;
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ if (getViewMin(child) >= left && getViewMax(child) <= right) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ }
+ // if we cannot find any, then just add all children.
+ if (views.size() == focusableCount) {
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ }
+ } else {
+ View view = findViewByPosition(mFocusPosition);
+ if (view != null) {
+ view.addFocusables(views, direction, focusableMode);
+ }
+ }
+ // if still cannot find any, fall through and add itself
+ if (views.size() != focusableCount) {
+ return true;
+ }
+ if (recyclerView.isFocusable()) {
+ views.add(recyclerView);
+ }
+ }
+ return true;
+ }
+
+ boolean hasCreatedLastItem() {
+ int count = getItemCount();
+ return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null;
+ }
+
+ boolean hasCreatedFirstItem() {
+ int count = getItemCount();
+ return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null;
+ }
+
+ boolean isItemFullyVisible(int pos) {
+ RecyclerView.ViewHolder vh = mBaseGridView.findViewHolderForAdapterPosition(pos);
+ if (vh == null) {
+ return false;
+ }
+ return vh.itemView.getLeft() >= 0 && vh.itemView.getRight() < mBaseGridView.getWidth()
+ && vh.itemView.getTop() >= 0 && vh.itemView.getBottom() < mBaseGridView.getHeight();
+ }
+
+ boolean canScrollTo(View view) {
+ return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
+ }
+
+ boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
+ Rect previouslyFocusedRect) {
+ switch (mFocusScrollStrategy) {
+ case BaseGridView.FOCUS_SCROLL_ALIGNED:
+ default:
+ return gridOnRequestFocusInDescendantsAligned(recyclerView,
+ direction, previouslyFocusedRect);
+ case BaseGridView.FOCUS_SCROLL_PAGE:
+ case BaseGridView.FOCUS_SCROLL_ITEM:
+ return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
+ direction, previouslyFocusedRect);
+ }
+ }
+
+ private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
+ int direction, Rect previouslyFocusedRect) {
+ View view = findViewByPosition(mFocusPosition);
+ if (view != null) {
+ boolean result = view.requestFocus(direction, previouslyFocusedRect);
+ if (!result && DEBUG) {
+ Log.w(getTag(), "failed to request focus on " + view);
+ }
+ return result;
+ }
+ return false;
+ }
+
+ private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
+ int direction, Rect previouslyFocusedRect) {
+ // focus to view not overlapping padding area to avoid scrolling in gaining focus
+ int index;
+ int increment;
+ int end;
+ int count = getChildCount();
+ if ((direction & View.FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ int left = mWindowAlignment.mainAxis().getPaddingMin();
+ int right = mWindowAlignment.mainAxis().getClientSize() + left;
+ for (int i = index; i != end; i += increment) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ if (getViewMin(child) >= left && getViewMax(child) <= right) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private final static int PREV_ITEM = 0;
+ private final static int NEXT_ITEM = 1;
+ private final static int PREV_ROW = 2;
+ private final static int NEXT_ROW = 3;
+
+ private int getMovement(int direction) {
+ int movement = View.FOCUS_LEFT;
+
+ if (mOrientation == HORIZONTAL) {
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? PREV_ITEM : NEXT_ITEM;
+ break;
+ case View.FOCUS_RIGHT:
+ movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? NEXT_ITEM : PREV_ITEM;
+ break;
+ case View.FOCUS_UP:
+ movement = PREV_ROW;
+ break;
+ case View.FOCUS_DOWN:
+ movement = NEXT_ROW;
+ break;
+ }
+ } else if (mOrientation == VERTICAL) {
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? PREV_ROW : NEXT_ROW;
+ break;
+ case View.FOCUS_RIGHT:
+ movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? NEXT_ROW : PREV_ROW;
+ break;
+ case View.FOCUS_UP:
+ movement = PREV_ITEM;
+ break;
+ case View.FOCUS_DOWN:
+ movement = NEXT_ITEM;
+ break;
+ }
+ }
+
+ return movement;
+ }
+
+ int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
+ View view = findViewByPosition(mFocusPosition);
+ if (view == null) {
+ return i;
+ }
+ int focusIndex = recyclerView.indexOfChild(view);
+ // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
+ // drawing order is 0 1 2 3 9 8 7 6 5 4
+ if (i < focusIndex) {
+ return i;
+ } else if (i < childCount - 1) {
+ return focusIndex + childCount - 1 - i;
+ } else {
+ return focusIndex;
+ }
+ }
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+ RecyclerView.Adapter newAdapter) {
+ if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
+ if (oldAdapter != null) {
+ discardLayoutInfo();
+ mFocusPosition = NO_POSITION;
+ mFocusPositionOffset = 0;
+ mChildrenStates.clear();
+ oldAdapter.unregisterAdapterDataObserver(mObServer);
+ }
+ if (newAdapter instanceof FacetProviderAdapter) {
+ mFacetProviderAdapter = (FacetProviderAdapter) newAdapter;
+ } else {
+ mFacetProviderAdapter = null;
+ }
+ if (newAdapter != null) {
+ newAdapter.registerAdapterDataObserver(mObServer);
+ }
+ super.onAdapterChanged(oldAdapter, newAdapter);
+ }
+
+ private void discardLayoutInfo() {
+ mGrid = null;
+ mRowSizeSecondary = null;
+ mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
+ }
+
+ public void setLayoutEnabled(boolean layoutEnabled) {
+ if (((mFlag & PF_LAYOUT_ENABLED) != 0) != layoutEnabled) {
+ mFlag = (mFlag & ~PF_LAYOUT_ENABLED) | (layoutEnabled ? PF_LAYOUT_ENABLED : 0);
+ requestLayout();
+ }
+ }
+
+ void setChildrenVisibility(int visibility) {
+ mChildVisibility = visibility;
+ if (mChildVisibility != -1) {
+ int count = getChildCount();
+ for (int i= 0; i < count; i++) {
+ getChildAt(i).setVisibility(mChildVisibility);
+ }
+ }
+ }
+
+ final static class SavedState implements Parcelable {
+
+ int index; // index inside adapter of the current view
+ Bundle childStates = Bundle.EMPTY;
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(index);
+ out.writeBundle(childStates);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ SavedState(Parcel in) {
+ index = in.readInt();
+ childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
+ }
+
+ SavedState() {
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
+ SavedState ss = new SavedState();
+ // save selected index
+ ss.index = getSelection();
+ // save offscreen child (state when they are recycled)
+ Bundle bundle = mChildrenStates.saveAsBundle();
+ // save views currently is on screen (TODO save cached views)
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ View view = getChildAt(i);
+ int position = getAdapterPositionByView(view);
+ if (position != NO_POSITION) {
+ bundle = mChildrenStates.saveOnScreenView(bundle, view, position);
+ }
+ }
+ ss.childStates = bundle;
+ return ss;
+ }
+
+ void onChildRecycled(RecyclerView.ViewHolder holder) {
+ final int position = holder.getAdapterPosition();
+ if (position != NO_POSITION) {
+ mChildrenStates.saveOffscreenView(holder.itemView, position);
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ return;
+ }
+ SavedState loadingState = (SavedState)state;
+ mFocusPosition = loadingState.index;
+ mFocusPositionOffset = 0;
+ mChildrenStates.loadFromBundle(loadingState.childStates);
+ mFlag |= PF_FORCE_FULL_LAYOUT;
+ requestLayout();
+ if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
+ }
+
+ @Override
+ public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL && mGrid != null) {
+ return mGrid.getNumRows();
+ }
+ return super.getRowCountForAccessibility(recycler, state);
+ }
+
+ @Override
+ public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (mOrientation == VERTICAL && mGrid != null) {
+ return mGrid.getNumRows();
+ }
+ return super.getColumnCountForAccessibility(recycler, state);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
+ RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
+ ViewGroup.LayoutParams lp = host.getLayoutParams();
+ if (mGrid == null || !(lp instanceof LayoutParams)) {
+ return;
+ }
+ LayoutParams glp = (LayoutParams) lp;
+ int position = glp.getViewAdapterPosition();
+ int rowIndex = position >= 0 ? mGrid.getRowIndex(position) : -1;
+ if (rowIndex < 0) {
+ return;
+ }
+ int guessSpanIndex = position / mGrid.getNumRows();
+ if (mOrientation == HORIZONTAL) {
+ info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
+ rowIndex, 1, guessSpanIndex, 1, false, false));
+ } else {
+ info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
+ guessSpanIndex, 1, rowIndex, 1, false, false));
+ }
+ }
+
+ /*
+ * Leanback widget is different than the default implementation because the "scroll" is driven
+ * by selection change.
+ */
+ @Override
+ public boolean performAccessibilityAction(Recycler recycler, State state, int action,
+ Bundle args) {
+ saveContext(recycler, state);
+ switch (action) {
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
+ processSelectionMoves(false, -1);
+ break;
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
+ processSelectionMoves(false, 1);
+ break;
+ }
+ leaveContext();
+ return true;
+ }
+
+ /*
+ * Move mFocusPosition multiple steps on the same row in main direction.
+ * Stops when moves are all consumed or reach first/last visible item.
+ * Returning remaining moves.
+ */
+ int processSelectionMoves(boolean preventScroll, int moves) {
+ if (mGrid == null) {
+ return moves;
+ }
+ int focusPosition = mFocusPosition;
+ int focusedRow = focusPosition != NO_POSITION
+ ? mGrid.getRowIndex(focusPosition) : NO_POSITION;
+ View newSelected = null;
+ for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) {
+ int index = moves > 0 ? i : count - 1 - i;
+ final View child = getChildAt(index);
+ if (!canScrollTo(child)) {
+ continue;
+ }
+ int position = getAdapterPositionByIndex(index);
+ int rowIndex = mGrid.getRowIndex(position);
+ if (focusedRow == NO_POSITION) {
+ focusPosition = position;
+ newSelected = child;
+ focusedRow = rowIndex;
+ } else if (rowIndex == focusedRow) {
+ if ((moves > 0 && position > focusPosition)
+ || (moves < 0 && position < focusPosition)) {
+ focusPosition = position;
+ newSelected = child;
+ if (moves > 0) {
+ moves--;
+ } else {
+ moves++;
+ }
+ }
+ }
+ }
+ if (newSelected != null) {
+ if (preventScroll) {
+ if (hasFocus()) {
+ mFlag |= PF_IN_SELECTION;
+ newSelected.requestFocus();
+ mFlag &= ~PF_IN_SELECTION;
+ }
+ mFocusPosition = focusPosition;
+ mSubFocusPosition = 0;
+ } else {
+ scrollToView(newSelected, true);
+ }
+ }
+ return moves;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
+ AccessibilityNodeInfoCompat info) {
+ saveContext(recycler, state);
+ int count = state.getItemCount();
+ if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(0)) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+ info.setScrollable(true);
+ }
+ if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(count - 1)) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ info.setScrollable(true);
+ }
+ final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
+ AccessibilityNodeInfoCompat.CollectionInfoCompat
+ .obtain(getRowCountForAccessibility(recycler, state),
+ getColumnCountForAccessibility(recycler, state),
+ isLayoutHierarchical(recycler, state),
+ getSelectionModeForAccessibility(recycler, state));
+ info.setCollectionInfo(collectionInfo);
+ leaveContext();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java b/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
rename to leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java b/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedAction.java
diff --git a/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
new file mode 100644
index 0000000..51b29e2
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
@@ -0,0 +1,557 @@
+/*
+ * 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 android.support.v17.leanback.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
+ * Presentation (view creation and state animation) is delegated to a {@link
+ * GuidedActionsStylist}, while clients are notified of interactions via
+ * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class GuidedActionAdapter extends RecyclerView.Adapter {
+ static final String TAG = "GuidedActionAdapter";
+ static final boolean DEBUG = false;
+
+ static final String TAG_EDIT = "EditableAction";
+ static final boolean DEBUG_EDIT = false;
+
+ /**
+ * Object listening for click events within a {@link GuidedActionAdapter}.
+ */
+ public interface ClickListener {
+
+ /**
+ * Called when the user clicks on an action.
+ */
+ void onGuidedActionClicked(GuidedAction action);
+
+ }
+
+ /**
+ * Object listening for focus events within a {@link GuidedActionAdapter}.
+ */
+ public interface FocusListener {
+
+ /**
+ * Called when the user focuses on an action.
+ */
+ void onGuidedActionFocused(GuidedAction action);
+ }
+
+ /**
+ * Object listening for edit events within a {@link GuidedActionAdapter}.
+ */
+ public interface EditListener {
+
+ /**
+ * Called when the user exits edit mode on an action.
+ */
+ void onGuidedActionEditCanceled(GuidedAction action);
+
+ /**
+ * Called when the user exits edit mode on an action and process confirm button in IME.
+ */
+ long onGuidedActionEditedAndProceed(GuidedAction action);
+
+ /**
+ * Called when Ime Open
+ */
+ void onImeOpen();
+
+ /**
+ * Called when Ime Close
+ */
+ void onImeClose();
+ }
+
+ private final boolean mIsSubAdapter;
+ private final ActionOnKeyListener mActionOnKeyListener;
+ private final ActionOnFocusListener mActionOnFocusListener;
+ private final ActionEditListener mActionEditListener;
+ private final List<GuidedAction> mActions;
+ private ClickListener mClickListener;
+ final GuidedActionsStylist mStylist;
+ GuidedActionAdapterGroup mGroup;
+ DiffCallback<GuidedAction> mDiffCallback;
+
+ private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v != null && v.getWindowToken() != null && getRecyclerView() != null) {
+ GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
+ getRecyclerView().getChildViewHolder(v);
+ GuidedAction action = avh.getAction();
+ if (action.hasTextEditable()) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
+ mGroup.openIme(GuidedActionAdapter.this, avh);
+ } else if (action.hasEditableActivatorView()) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
+ performOnActionClick(avh);
+ } else {
+ handleCheckedActions(avh);
+ if (action.isEnabled() && !action.infoOnly()) {
+ performOnActionClick(avh);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
+ * focus listeners, and the given presenter.
+ * @param actions The list of guided actions this adapter will manage.
+ * @param focusListener The focus listener for items in this adapter.
+ * @param presenter The presenter that will manage the display of items in this adapter.
+ */
+ public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
+ FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) {
+ super();
+ mActions = actions == null ? new ArrayList<GuidedAction>() :
+ new ArrayList<GuidedAction>(actions);
+ mClickListener = clickListener;
+ mStylist = presenter;
+ mActionOnKeyListener = new ActionOnKeyListener();
+ mActionOnFocusListener = new ActionOnFocusListener(focusListener);
+ mActionEditListener = new ActionEditListener();
+ mIsSubAdapter = isSubAdapter;
+ if (!isSubAdapter) {
+ mDiffCallback = GuidedActionDiffCallback.getInstance();
+ }
+ }
+
+ /**
+ * Change DiffCallback used in {@link #setActions(List)}. Set to null for firing a
+ * general {@link #notifyDataSetChanged()}.
+ *
+ * @param diffCallback
+ */
+ public void setDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+ mDiffCallback = diffCallback;
+ }
+
+ /**
+ * Sets the list of actions managed by this adapter. Use {@link #setDiffCallback(DiffCallback)}
+ * to change DiffCallback.
+ * @param actions The list of actions to be managed.
+ */
+ public void setActions(final List<GuidedAction> actions) {
+ if (!mIsSubAdapter) {
+ mStylist.collapseAction(false);
+ }
+ mActionOnFocusListener.unFocus();
+ if (mDiffCallback != null) {
+ // temporary variable used for DiffCallback
+ final List<GuidedAction> oldActions = new ArrayList();
+ oldActions.addAll(mActions);
+
+ // update items.
+ mActions.clear();
+ mActions.addAll(actions);
+
+ DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
+ @Override
+ public int getOldListSize() {
+ return oldActions.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return mActions.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return mDiffCallback.areItemsTheSame(oldActions.get(oldItemPosition),
+ mActions.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return mDiffCallback.areContentsTheSame(oldActions.get(oldItemPosition),
+ mActions.get(newItemPosition));
+ }
+
+ @Nullable
+ @Override
+ public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ return mDiffCallback.getChangePayload(oldActions.get(oldItemPosition),
+ mActions.get(newItemPosition));
+ }
+ });
+
+ // dispatch diff result
+ diffResult.dispatchUpdatesTo(this);
+ } else {
+ mActions.clear();
+ mActions.addAll(actions);
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Returns the count of actions managed by this adapter.
+ * @return The count of actions managed by this adapter.
+ */
+ public int getCount() {
+ return mActions.size();
+ }
+
+ /**
+ * Returns the GuidedAction at the given position in the managed list.
+ * @param position The position of the desired GuidedAction.
+ * @return The GuidedAction at the given position.
+ */
+ public GuidedAction getItem(int position) {
+ return mActions.get(position);
+ }
+
+ /**
+ * Return index of action in array
+ * @param action Action to search index.
+ * @return Index of Action in array.
+ */
+ public int indexOf(GuidedAction action) {
+ return mActions.indexOf(action);
+ }
+
+ /**
+ * @return GuidedActionsStylist used to build the actions list UI.
+ */
+ public GuidedActionsStylist getGuidedActionsStylist() {
+ return mStylist;
+ }
+
+ /**
+ * Sets the click listener for items managed by this adapter.
+ * @param clickListener The click listener for this adapter.
+ */
+ public void setClickListener(ClickListener clickListener) {
+ mClickListener = clickListener;
+ }
+
+ /**
+ * Sets the focus listener for items managed by this adapter.
+ * @param focusListener The focus listener for this adapter.
+ */
+ public void setFocusListener(FocusListener focusListener) {
+ mActionOnFocusListener.setFocusListener(focusListener);
+ }
+
+ /**
+ * Used for serialization only.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public List<GuidedAction> getActions() {
+ return new ArrayList<GuidedAction>(mActions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return mStylist.getItemViewType(mActions.get(position));
+ }
+
+ RecyclerView getRecyclerView() {
+ return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType);
+ View v = vh.itemView;
+ v.setOnKeyListener(mActionOnKeyListener);
+ v.setOnClickListener(mOnClickListener);
+ v.setOnFocusChangeListener(mActionOnFocusListener);
+
+ setupListeners(vh.getEditableTitleView());
+ setupListeners(vh.getEditableDescriptionView());
+
+ return vh;
+ }
+
+ private void setupListeners(EditText edit) {
+ if (edit != null) {
+ edit.setPrivateImeOptions("EscapeNorth=1;");
+ edit.setOnEditorActionListener(mActionEditListener);
+ if (edit instanceof ImeKeyMonitor) {
+ ImeKeyMonitor monitor = (ImeKeyMonitor)edit;
+ monitor.setImeKeyListener(mActionEditListener);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (position >= mActions.size()) {
+ return;
+ }
+ final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder;
+ GuidedAction action = mActions.get(position);
+ mStylist.onBindViewHolder(avh, action);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemCount() {
+ return mActions.size();
+ }
+
+ private class ActionOnFocusListener implements View.OnFocusChangeListener {
+
+ private FocusListener mFocusListener;
+ private View mSelectedView;
+
+ ActionOnFocusListener(FocusListener focusListener) {
+ mFocusListener = focusListener;
+ }
+
+ public void setFocusListener(FocusListener focusListener) {
+ mFocusListener = focusListener;
+ }
+
+ public void unFocus() {
+ if (mSelectedView != null && getRecyclerView() != null) {
+ ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView);
+ if (vh != null) {
+ GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh;
+ mStylist.onAnimateItemFocused(avh, false);
+ } else {
+ Log.w(TAG, "RecyclerView returned null view holder",
+ new Throwable());
+ }
+ }
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (getRecyclerView() == null) {
+ return;
+ }
+ GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
+ getRecyclerView().getChildViewHolder(v);
+ if (hasFocus) {
+ mSelectedView = v;
+ if (mFocusListener != null) {
+ // We still call onGuidedActionFocused so that listeners can clear
+ // state if they want.
+ mFocusListener.onGuidedActionFocused(avh.getAction());
+ }
+ } else {
+ if (mSelectedView == v) {
+ mStylist.onAnimateItemPressedCancelled(avh);
+ mSelectedView = null;
+ }
+ }
+ mStylist.onAnimateItemFocused(avh, hasFocus);
+ }
+ }
+
+ public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) {
+ // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy
+ if (getRecyclerView() == null) {
+ return null;
+ }
+ GuidedActionsStylist.ViewHolder result = null;
+ ViewParent parent = v.getParent();
+ while (parent != getRecyclerView() && parent != null && v != null) {
+ v = (View)parent;
+ parent = parent.getParent();
+ }
+ if (parent != null && v != null) {
+ result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v);
+ }
+ return result;
+ }
+
+ public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) {
+ GuidedAction action = avh.getAction();
+ int actionCheckSetId = action.getCheckSetId();
+ if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) {
+ // Find any actions that are checked and are in the same group
+ // as the selected action. Fade their checkmarks out.
+ if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ for (int i = 0, size = mActions.size(); i < size; i++) {
+ GuidedAction a = mActions.get(i);
+ if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
+ a.setChecked(false);
+ GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder)
+ getRecyclerView().findViewHolderForPosition(i);
+ if (vh != null) {
+ mStylist.onAnimateItemChecked(vh, false);
+ }
+ }
+ }
+ }
+
+ // If we we'ren't already checked, fade our checkmark in.
+ if (!action.isChecked()) {
+ action.setChecked(true);
+ mStylist.onAnimateItemChecked(avh, true);
+ } else {
+ if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ action.setChecked(false);
+ mStylist.onAnimateItemChecked(avh, false);
+ }
+ }
+ }
+ }
+
+ public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) {
+ if (mClickListener != null) {
+ mClickListener.onGuidedActionClicked(avh.getAction());
+ }
+ }
+
+ private class ActionOnKeyListener implements View.OnKeyListener {
+
+ private boolean mKeyPressed = false;
+
+ ActionOnKeyListener() {
+ }
+
+ /**
+ * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
+ */
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (v == null || event == null || getRecyclerView() == null) {
+ return false;
+ }
+ boolean handled = false;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_NUMPAD_ENTER:
+ case KeyEvent.KEYCODE_BUTTON_X:
+ case KeyEvent.KEYCODE_BUTTON_Y:
+ case KeyEvent.KEYCODE_ENTER:
+
+ GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
+ getRecyclerView().getChildViewHolder(v);
+ GuidedAction action = avh.getAction();
+
+ if (!action.isEnabled() || action.infoOnly()) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ // TODO: requires API 19
+ //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
+ }
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (DEBUG) {
+ Log.d(TAG, "Enter Key down");
+ }
+ if (!mKeyPressed) {
+ mKeyPressed = true;
+ mStylist.onAnimateItemPressed(avh, mKeyPressed);
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ if (DEBUG) {
+ Log.d(TAG, "Enter Key up");
+ }
+ // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
+ // Escape in IME.
+ if (mKeyPressed) {
+ mKeyPressed = false;
+ mStylist.onAnimateItemPressed(avh, mKeyPressed);
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ return handled;
+ }
+
+ }
+
+ private class ActionEditListener implements OnEditorActionListener,
+ ImeKeyMonitor.ImeKeyListener {
+
+ ActionEditListener() {
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
+ boolean handled = false;
+ if (actionId == EditorInfo.IME_ACTION_NEXT
+ || actionId == EditorInfo.IME_ACTION_DONE) {
+ mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
+ handled = true;
+ } else if (actionId == EditorInfo.IME_ACTION_NONE) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
+ // Escape north handling: stay on current item, but close editor
+ handled = true;
+ mGroup.fillAndStay(GuidedActionAdapter.this, v);
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+ mGroup.fillAndStay(GuidedActionAdapter.this, editText);
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_ENTER
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
+ return true;
+ }
+ return false;
+ }
+
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
diff --git a/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java
new file mode 100644
index 0000000..d4d4d77
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+/**
+ * DiffCallback used for GuidedActions, see {@link
+ * android.support.v17.leanback.app.GuidedStepSupportFragment#setActionsDiffCallback(DiffCallback)}.
+ */
+public class GuidedActionDiffCallback extends DiffCallback<GuidedAction> {
+
+ static final GuidedActionDiffCallback sInstance = new GuidedActionDiffCallback();
+
+ /**
+ * Returns the singleton GuidedActionDiffCallback.
+ * @return The singleton GuidedActionDiffCallback.
+ */
+ public static final GuidedActionDiffCallback getInstance() {
+ return sInstance;
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull GuidedAction oldItem, @NonNull GuidedAction newItem) {
+ if (oldItem == null) {
+ return newItem == null;
+ } else if (newItem == null) {
+ return false;
+ }
+ return oldItem.getId() == newItem.getId();
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull GuidedAction oldItem,
+ @NonNull GuidedAction newItem) {
+ if (oldItem == null) {
+ return newItem == null;
+ } else if (newItem == null) {
+ return false;
+ }
+ return oldItem.getCheckSetId() == newItem.getCheckSetId()
+ && oldItem.mActionFlags == newItem.mActionFlags
+ && TextUtils.equals(oldItem.getTitle(), newItem.getTitle())
+ && TextUtils.equals(oldItem.getDescription(), newItem.getDescription())
+ && oldItem.getInputType() == newItem.getInputType()
+ && TextUtils.equals(oldItem.getEditTitle(), newItem.getEditTitle())
+ && TextUtils.equals(oldItem.getEditDescription(), newItem.getEditDescription())
+ && oldItem.getEditInputType() == newItem.getEditInputType()
+ && oldItem.getDescriptionEditInputType() == newItem.getDescriptionEditInputType();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java b/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java b/leanback/src/android/support/v17/leanback/widget/HeaderItem.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java
rename to leanback/src/android/support/v17/leanback/widget/HeaderItem.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java b/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
rename to leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java b/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
rename to leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
rename to leanback/src/android/support/v17/leanback/widget/ImageCardView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java b/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
rename to leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java b/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java b/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
rename to leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRow.java b/leanback/src/android/support/v17/leanback/widget/ListRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRow.java
rename to leanback/src/android/support/v17/leanback/widget/ListRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java b/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java b/leanback/src/android/support/v17/leanback/widget/ListRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java b/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java b/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
rename to leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java b/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
rename to leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java b/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
rename to leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
diff --git a/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
new file mode 100644
index 0000000..d411f9e
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.database.Observable;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Base class adapter to be used in leanback activities. Provides access to a data model and is
+ * decoupled from the presentation of the items via {@link PresenterSelector}.
+ */
+public abstract class ObjectAdapter {
+
+ /** Indicates that an id has not been set. */
+ public static final int NO_ID = -1;
+
+ /**
+ * A DataObserver can be notified when an ObjectAdapter's underlying data
+ * changes. Separate methods provide notifications about different types of
+ * changes.
+ */
+ public static abstract class DataObserver {
+ /**
+ * Called whenever the ObjectAdapter's data has changed in some manner
+ * outside of the set of changes covered by the other range-based change
+ * notification methods.
+ */
+ public void onChanged() {
+ }
+
+ /**
+ * Called when a range of items in the ObjectAdapter has changed. The
+ * basic ordering and structure of the ObjectAdapter has not changed.
+ *
+ * @param positionStart The position of the first item that changed.
+ * @param itemCount The number of items changed.
+ */
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ /**
+ * Called when a range of items in the ObjectAdapter has changed. The
+ * basic ordering and structure of the ObjectAdapter has not changed.
+ *
+ * @param positionStart The position of the first item that changed.
+ * @param itemCount The number of items changed.
+ * @param payload Optional parameter, use null to identify a "full" update.
+ */
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ onChanged();
+ }
+
+ /**
+ * Called when a range of items is inserted into the ObjectAdapter.
+ *
+ * @param positionStart The position of the first inserted item.
+ * @param itemCount The number of items inserted.
+ */
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ /**
+ * Called when an item is moved from one position to another position
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ public void onItemMoved(int fromPosition, int toPosition) {
+ onChanged();
+ }
+
+ /**
+ * Called when a range of items is removed from the ObjectAdapter.
+ *
+ * @param positionStart The position of the first removed item.
+ * @param itemCount The number of items removed.
+ */
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ onChanged();
+ }
+ }
+
+ private static final class DataObservable extends Observable<DataObserver> {
+
+ DataObservable() {
+ }
+
+ public void notifyChanged() {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
+ }
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
+ }
+ }
+
+ public void notifyItemRangeInserted(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemMoved(int positionStart, int toPosition) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemMoved(positionStart, toPosition);
+ }
+ }
+
+ boolean hasObserver() {
+ return mObservers.size() > 0;
+ }
+ }
+
+ private final DataObservable mObservable = new DataObservable();
+ private boolean mHasStableIds;
+ private PresenterSelector mPresenterSelector;
+
+ /**
+ * Constructs an adapter with the given {@link PresenterSelector}.
+ */
+ public ObjectAdapter(PresenterSelector presenterSelector) {
+ setPresenterSelector(presenterSelector);
+ }
+
+ /**
+ * Constructs an adapter that uses the given {@link Presenter} for all items.
+ */
+ public ObjectAdapter(Presenter presenter) {
+ setPresenterSelector(new SinglePresenterSelector(presenter));
+ }
+
+ /**
+ * Constructs an adapter.
+ */
+ public ObjectAdapter() {
+ }
+
+ /**
+ * Sets the presenter selector. May not be null.
+ */
+ public final void setPresenterSelector(PresenterSelector presenterSelector) {
+ if (presenterSelector == null) {
+ throw new IllegalArgumentException("Presenter selector must not be null");
+ }
+ final boolean update = (mPresenterSelector != null);
+ final boolean selectorChanged = update && mPresenterSelector != presenterSelector;
+
+ mPresenterSelector = presenterSelector;
+
+ if (selectorChanged) {
+ onPresenterSelectorChanged();
+ }
+ if (update) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Called when {@link #setPresenterSelector(PresenterSelector)} is called
+ * and the PresenterSelector differs from the previous one.
+ */
+ protected void onPresenterSelectorChanged() {
+ }
+
+ /**
+ * Returns the presenter selector for this ObjectAdapter.
+ */
+ public final PresenterSelector getPresenterSelector() {
+ return mPresenterSelector;
+ }
+
+ /**
+ * Registers a DataObserver for data change notifications.
+ */
+ public final void registerObserver(DataObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregisters a DataObserver for data change notifications.
+ */
+ public final void unregisterObserver(DataObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final boolean hasObserver() {
+ return mObservable.hasObserver();
+ }
+
+ /**
+ * Unregisters all DataObservers for this ObjectAdapter.
+ */
+ public final void unregisterAllObservers() {
+ mObservable.unregisterAll();
+ }
+
+ /**
+ * Notifies UI that some items has changed.
+ *
+ * @param positionStart Starting position of the changed items.
+ * @param itemCount Total number of items that changed.
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Notifies UI that some items has changed.
+ *
+ * @param positionStart Starting position of the changed items.
+ * @param itemCount Total number of items that changed.
+ * @param payload Optional parameter, use null to identify a "full" update.
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
+ }
+
+ /**
+ * Notifies UI that new items has been inserted.
+ *
+ * @param positionStart Position where new items has been inserted.
+ * @param itemCount Count of the new items has been inserted.
+ */
+ final protected void notifyItemRangeInserted(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ /**
+ * Notifies UI that some items that has been removed.
+ *
+ * @param positionStart Starting position of the removed items.
+ * @param itemCount Total number of items that has been removed.
+ */
+ final protected void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeRemoved(positionStart, itemCount);
+ }
+
+ /**
+ * Notifies UI that item at fromPosition has been moved to toPosition.
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ protected final void notifyItemMoved(int fromPosition, int toPosition) {
+ mObservable.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /**
+ * Notifies UI that the underlying data has changed.
+ */
+ final protected void notifyChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * Returns true if the item ids are stable across changes to the
+ * underlying data. When this is true, clients of the ObjectAdapter can use
+ * {@link #getId(int)} to correlate Objects across changes.
+ */
+ public final boolean hasStableIds() {
+ return mHasStableIds;
+ }
+
+ /**
+ * Sets whether the item ids are stable across changes to the underlying
+ * data.
+ */
+ public final void setHasStableIds(boolean hasStableIds) {
+ boolean changed = mHasStableIds != hasStableIds;
+ mHasStableIds = hasStableIds;
+
+ if (changed) {
+ onHasStableIdsChanged();
+ }
+ }
+
+ /**
+ * Called when {@link #setHasStableIds(boolean)} is called and the status
+ * of stable ids has changed.
+ */
+ protected void onHasStableIdsChanged() {
+ }
+
+ /**
+ * Returns the {@link Presenter} for the given item from the adapter.
+ */
+ public final Presenter getPresenter(Object item) {
+ if (mPresenterSelector == null) {
+ throw new IllegalStateException("Presenter selector must not be null");
+ }
+ return mPresenterSelector.getPresenter(item);
+ }
+
+ /**
+ * Returns the number of items in the adapter.
+ */
+ public abstract int size();
+
+ /**
+ * Returns the item for the given position.
+ */
+ public abstract Object get(int position);
+
+ /**
+ * Returns the id for the given position.
+ */
+ public long getId(int position) {
+ return NO_ID;
+ }
+
+ /**
+ * Returns true if the adapter pairs each underlying data change with a call to notify and
+ * false otherwise.
+ */
+ public boolean isImmediateNotifySupported() {
+ return false;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java b/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java b/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PageRow.java b/leanback/src/android/support/v17/leanback/widget/PageRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PageRow.java
rename to leanback/src/android/support/v17/leanback/widget/PageRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java b/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
rename to leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java b/leanback/src/android/support/v17/leanback/widget/Parallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Parallax.java
rename to leanback/src/android/support/v17/leanback/widget/Parallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java b/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
rename to leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java b/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
rename to leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java b/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
rename to leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java b/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java b/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java b/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Presenter.java b/leanback/src/android/support/v17/leanback/widget/Presenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Presenter.java
rename to leanback/src/android/support/v17/leanback/widget/Presenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java b/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
rename to leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java b/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
rename to leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java b/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
rename to leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java b/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
rename to leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Row.java b/leanback/src/android/support/v17/leanback/widget/Row.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Row.java
rename to leanback/src/android/support/v17/leanback/widget/Row.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java b/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
rename to leanback/src/android/support/v17/leanback/widget/RowContainerView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java b/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java b/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
rename to leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java b/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/RowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/leanback/src/android/support/v17/leanback/widget/SearchBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
rename to leanback/src/android/support/v17/leanback/widget/SearchBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java b/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
rename to leanback/src/android/support/v17/leanback/widget/SearchEditText.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java b/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
rename to leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SectionRow.java b/leanback/src/android/support/v17/leanback/widget/SectionRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SectionRow.java
rename to leanback/src/android/support/v17/leanback/widget/SectionRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java b/leanback/src/android/support/v17/leanback/widget/SeekBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java
rename to leanback/src/android/support/v17/leanback/widget/SeekBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java b/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java b/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java b/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java b/leanback/src/android/support/v17/leanback/widget/SingleRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
rename to leanback/src/android/support/v17/leanback/widget/SingleRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java b/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
rename to leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java b/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
rename to leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
rename to leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java b/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
rename to leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java b/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
rename to leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java b/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
rename to leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java b/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
rename to leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleHelper.java b/leanback/src/android/support/v17/leanback/widget/TitleHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleHelper.java
rename to leanback/src/android/support/v17/leanback/widget/TitleHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java b/leanback/src/android/support/v17/leanback/widget/TitleView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
rename to leanback/src/android/support/v17/leanback/widget/TitleView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java b/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Util.java b/leanback/src/android/support/v17/leanback/widget/Util.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Util.java
rename to leanback/src/android/support/v17/leanback/widget/Util.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java b/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java b/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
rename to leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java b/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
rename to leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java b/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
rename to leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java b/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
rename to leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Visibility.java b/leanback/src/android/support/v17/leanback/widget/Visibility.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Visibility.java
rename to leanback/src/android/support/v17/leanback/widget/Visibility.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java b/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
rename to leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/package-info.java b/leanback/src/android/support/v17/leanback/widget/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/package-info.java
rename to leanback/src/android/support/v17/leanback/widget/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java b/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/Picker.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java b/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
rename to leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java b/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
rename to leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java b/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
diff --git a/v17/leanback/tests/AndroidManifest.xml b/leanback/tests/AndroidManifest.xml
similarity index 100%
rename from v17/leanback/tests/AndroidManifest.xml
rename to leanback/tests/AndroidManifest.xml
diff --git a/v17/leanback/tests/NO_DOCS b/leanback/tests/NO_DOCS
similarity index 100%
rename from v17/leanback/tests/NO_DOCS
rename to leanback/tests/NO_DOCS
diff --git a/leanback/tests/generatev4.py b/leanback/tests/generatev4.py
new file mode 100755
index 0000000..d7d14a8
--- /dev/null
+++ b/leanback/tests/generatev4.py
@@ -0,0 +1,168 @@
+#!/usr/bin/python
+
+# 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.
+
+import os
+import sys
+
+print "Generate v4 fragment related code for leanback"
+
+####### generate XXXTestFragment classes #######
+
+files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest', 'DetailsTest']
+
+cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
+ 'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
+ 'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video',
+ 'DetailsTest']
+
+for w in files:
+ print "copy {}SupportFragment to {}Fragment".format(w, w)
+
+ file = open('java/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'r')
+ outfile = open('java/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'w')
+
+ outfile.write("// CHECKSTYLE:OFF Generated code\n")
+ outfile.write("/* This file is auto-generated from {}SupportFragment.java. DO NOT MODIFY. */\n\n".format(w))
+
+ for line in file:
+ for w in cls:
+ line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+ line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('FragmentActivity getActivity()', 'Activity getActivity()')
+ outfile.write(line)
+ file.close()
+ outfile.close()
+
+####### generate XXXFragmentTestBase classes #######
+
+testcls = ['GuidedStep', 'Single']
+
+for w in testcls:
+ print "copy {}SupportFrgamentTestBase to {}FragmentTestBase".format(w, w)
+
+ file = open('java/android/support/v17/leanback/app/{}SupportFragmentTestBase.java'.format(w), 'r')
+ outfile = open('java/android/support/v17/leanback/app/{}FragmentTestBase.java'.format(w), 'w')
+
+ outfile.write("// CHECKSTYLE:OFF Generated code\n")
+ outfile.write("/* This file is auto-generated from {}SupportFrgamentTestBase.java. DO NOT MODIFY. */\n\n".format(w))
+
+ for line in file:
+ for w in cls:
+ line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+ for w in testcls:
+ line = line.replace('{}SupportFragmentTestBase'.format(w), '{}FragmentTestBase'.format(w))
+ line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+ line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+ line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ outfile.write(line)
+ file.close()
+ outfile.close()
+
+####### generate XXXFragmentTest classes #######
+
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
+
+for w in testcls:
+ print "copy {}SupporFrgamentTest to {}tFragmentTest".format(w, w)
+
+ file = open('java/android/support/v17/leanback/app/{}SupportFragmentTest.java'.format(w), 'r')
+ outfile = open('java/android/support/v17/leanback/app/{}FragmentTest.java'.format(w), 'w')
+
+ outfile.write("// CHECKSTYLE:OFF Generated code\n")
+ outfile.write("/* This file is auto-generated from {}SupportFragmentTest.java. DO NOT MODIFY. */\n\n".format(w))
+
+ for line in file:
+ for w in cls:
+ line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+ for w in testcls:
+ line = line.replace('SingleSupportFragmentTestBase', 'SingleFragmentTestBase')
+ line = line.replace('SingleSupportFragmentTestActivity', 'SingleFragmentTestActivity')
+ line = line.replace('{}SupportFragmentTestBase'.format(w), '{}FragmentTestBase'.format(w))
+ line = line.replace('{}SupportFragmentTest'.format(w), '{}FragmentTest'.format(w))
+ line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+ line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+ line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('extends FragmentActivity', 'extends Activity')
+ line = line.replace('Activity.this.getSupportFragmentManager', 'Activity.this.getFragmentManager')
+ line = line.replace('tivity.getSupportFragmentManager', 'tivity.getFragmentManager')
+ outfile.write(line)
+ file.close()
+ outfile.close()
+
+
+####### generate XXXTestActivity classes #######
+testcls = ['Browse', 'GuidedStep', 'Single']
+
+for w in testcls:
+ print "copy {}SupportFragmentTestActivity to {}FragmentTestActivity".format(w, w)
+ file = open('java/android/support/v17/leanback/app/{}SupportFragmentTestActivity.java'.format(w), 'r')
+ outfile = open('java/android/support/v17/leanback/app/{}FragmentTestActivity.java'.format(w), 'w')
+ outfile.write("// CHECKSTYLE:OFF Generated code\n")
+ outfile.write("/* This file is auto-generated from {}SupportFragmentTestActivity.java. DO NOT MODIFY. */\n\n".format(w))
+ for line in file:
+ line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+ line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+ line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+ line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+ line = line.replace('extends FragmentActivity', 'extends Activity')
+ line = line.replace('getSupportFragmentManager', 'getFragmentManager')
+ outfile.write(line)
+ file.close()
+ outfile.close()
+
+####### generate Float parallax test #######
+
+print "copy ParallaxIntEffectTest to ParallaxFloatEffectTest"
+file = open('java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java', 'r')
+outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java', 'w')
+outfile.write("// CHECKSTYLE:OFF Generated code\n")
+outfile.write("/* This file is auto-generated from ParallaxIntEffectTest.java. DO NOT MODIFY. */\n\n")
+for line in file:
+ line = line.replace('IntEffect', 'FloatEffect')
+ line = line.replace('IntParallax', 'FloatParallax')
+ line = line.replace('IntProperty', 'FloatProperty')
+ line = line.replace('intValue()', 'floatValue()')
+ line = line.replace('int screenMax', 'float screenMax')
+ line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
+ line = line.replace('(int)', '(float)')
+ line = line.replace('int[', 'float[')
+ line = line.replace('Integer', 'Float');
+ outfile.write(line)
+file.close()
+outfile.close()
+
+
+print "copy ParallaxIntTest to ParallaxFloatTest"
+file = open('java/android/support/v17/leanback/widget/ParallaxIntTest.java', 'r')
+outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatTest.java', 'w')
+outfile.write("// CHECKSTYLE:OFF Generated code\n")
+outfile.write("/* This file is auto-generated from ParallaxIntTest.java. DO NOT MODIFY. */\n\n")
+for line in file:
+ line = line.replace('ParallaxIntTest', 'ParallaxFloatTest')
+ line = line.replace('IntParallax', 'FloatParallax')
+ line = line.replace('IntProperty', 'FloatProperty')
+ line = line.replace('verifyIntProperties', 'verifyFloatProperties')
+ line = line.replace('intValue()', 'floatValue()')
+ line = line.replace('int screenMax', 'float screenMax')
+ line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
+ line = line.replace('(int)', '(float)')
+ outfile.write(line)
+file.close()
+outfile.close()
+
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java b/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
new file mode 100644
index 0000000..654bbe7
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
@@ -0,0 +1,257 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class BrowseFragmentTest {
+
+ static final String TAG = "BrowseFragmentTest";
+ static final long WAIT_TRANSIITON_TIMEOUT = 10000;
+
+ @Rule
+ public ActivityTestRule<BrowseFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(BrowseFragmentTestActivity.class, false, false);
+ private BrowseFragmentTestActivity mActivity;
+
+ @After
+ public void afterTest() throws Throwable {
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mActivity != null) {
+ mActivity.finish();
+ mActivity = null;
+ }
+ }
+ });
+ }
+
+ void waitForEntranceTransitionFinished() {
+ PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return mActivity.getBrowseTestFragment() != null
+ && mActivity.getBrowseTestFragment().mEntranceTransitionEnded;
+ } else {
+ // when entrance transition not supported, wait main fragment loaded.
+ return mActivity.getBrowseTestFragment() != null
+ && mActivity.getBrowseTestFragment().getMainFragment() != null;
+ }
+ }
+ });
+ }
+
+ void waitForHeaderTransitionFinished() {
+ View row = mActivity.getBrowseTestFragment().getRowsFragment().getRowViewHolder(
+ mActivity.getBrowseTestFragment().getSelectedPosition()).view;
+ PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.ViewStableOnScreen(row));
+ }
+
+ @Test
+ public void testTwoBackKeysWithBackStack() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ assertNotNull(mActivity.getBrowseTestFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ waitForHeaderTransitionFinished();
+ sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
+ }
+
+ @Test
+ public void testTwoBackKeysWithoutBackStack() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ assertNotNull(mActivity.getBrowseTestFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ waitForHeaderTransitionFinished();
+ sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
+ }
+
+ @Test
+ public void testPressRightBeforeMainFragmentCreated() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ assertNull(mActivity.getBrowseTestFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ }
+
+ @Test
+ public void testSelectCardOnARow() throws Throwable {
+ final int selectRow = 10;
+ final int selectItem = 20;
+ Intent intent = new Intent();
+ final long dataLoadingDelay = 1000;
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ Presenter.ViewHolderTask itemTask = Mockito.spy(
+ new ItemSelectionTask(mActivity, selectRow));
+
+ final ListRowPresenter.SelectItemViewHolderTask task =
+ new ListRowPresenter.SelectItemViewHolderTask(selectItem);
+ task.setItemTask(itemTask);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getBrowseTestFragment().setSelectedPosition(selectRow, true, task);
+ }
+ });
+
+ verify(itemTask, timeout(5000).times(1)).run(any(Presenter.ViewHolder.class));
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ListRowPresenter.ViewHolder row = (ListRowPresenter.ViewHolder) mActivity
+ .getBrowseTestFragment().getRowsFragment().getRowViewHolder(selectRow);
+ assertNotNull(dumpRecyclerView(mActivity.getBrowseTestFragment().getGridView()), row);
+ assertNotNull(row.getGridView());
+ assertEquals(selectItem, row.getGridView().getSelectedPosition());
+ }
+ });
+ }
+
+ @Test
+ public void activityRecreate_notCrash() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD, true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ InstrumentationRegistry.getInstrumentation().callActivityOnRestart(mActivity);
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.recreate();
+ }
+ });
+ }
+
+
+ @Test
+ public void lateLoadingHeaderDisabled() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseFragmentTestActivity.EXTRA_HEADERS_STATE,
+ BrowseFragment.HEADERS_DISABLED);
+ mActivity = activityTestRule.launchActivity(intent);
+ waitForEntranceTransitionFinished();
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mActivity.getBrowseTestFragment().getGridView() != null
+ && mActivity.getBrowseTestFragment().getGridView().getChildCount() > 0;
+ }
+ });
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ public static class ItemSelectionTask extends Presenter.ViewHolderTask {
+
+ private final BrowseFragmentTestActivity activity;
+ private final int expectedRow;
+
+ public ItemSelectionTask(BrowseFragmentTestActivity activity, int expectedRow) {
+ this.activity = activity;
+ this.expectedRow = expectedRow;
+ }
+
+ @Override
+ public void run(Presenter.ViewHolder holder) {
+ android.util.Log.d(TAG, dumpRecyclerView(activity.getBrowseTestFragment()
+ .getGridView()));
+ android.util.Log.d(TAG, "Row " + expectedRow + " " + activity.getBrowseTestFragment()
+ .getRowsFragment().getRowViewHolder(expectedRow), new Exception());
+ }
+ }
+
+ static String dumpRecyclerView(RecyclerView recyclerView) {
+ StringBuffer b = new StringBuffer();
+ for (int i = 0; i < recyclerView.getChildCount(); i++) {
+ View child = recyclerView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ recyclerView.getChildViewHolder(child);
+ b.append("child").append(i).append(":").append(vh);
+ if (vh != null) {
+ b.append(",").append(vh.getViewHolder());
+ }
+ b.append(";");
+ }
+ return b.toString();
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
new file mode 100644
index 0000000..3ce025d
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
@@ -0,0 +1,60 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseSupportFragmentTestActivity.java. DO NOT MODIFY. */
+
+/*
+ * 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 android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.app.Activity;
+import android.app.FragmentTransaction;
+
+public class BrowseFragmentTestActivity extends Activity {
+
+ public static final String EXTRA_ADD_TO_BACKSTACK = "addToBackStack";
+ public static final String EXTRA_NUM_ROWS = "numRows";
+ public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
+ public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
+ public static final String EXTRA_TEST_ENTRANCE_TRANSITION = "testEntranceTransition";
+ public static final String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
+ public static final String EXTRA_HEADERS_STATE = "headers_state";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ setContentView(R.layout.browse);
+ if (savedInstanceState == null) {
+ Bundle arguments = new Bundle();
+ arguments.putAll(intent.getExtras());
+ BrowseTestFragment fragment = new BrowseTestFragment();
+ fragment.setArguments(arguments);
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ if (intent.getBooleanExtra(EXTRA_ADD_TO_BACKSTACK, false)) {
+ ft.addToBackStack(null);
+ }
+ ft.commit();
+ }
+ }
+
+ public BrowseTestFragment getBrowseTestFragment() {
+ return (BrowseTestFragment) getFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
new file mode 100644
index 0000000..51151ae
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class BrowseSupportFragmentTest {
+
+ static final String TAG = "BrowseSupportFragmentTest";
+ static final long WAIT_TRANSIITON_TIMEOUT = 10000;
+
+ @Rule
+ public ActivityTestRule<BrowseSupportFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(BrowseSupportFragmentTestActivity.class, false, false);
+ private BrowseSupportFragmentTestActivity mActivity;
+
+ @After
+ public void afterTest() throws Throwable {
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mActivity != null) {
+ mActivity.finish();
+ mActivity = null;
+ }
+ }
+ });
+ }
+
+ void waitForEntranceTransitionFinished() {
+ PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return mActivity.getBrowseTestSupportFragment() != null
+ && mActivity.getBrowseTestSupportFragment().mEntranceTransitionEnded;
+ } else {
+ // when entrance transition not supported, wait main fragment loaded.
+ return mActivity.getBrowseTestSupportFragment() != null
+ && mActivity.getBrowseTestSupportFragment().getMainFragment() != null;
+ }
+ }
+ });
+ }
+
+ void waitForHeaderTransitionFinished() {
+ View row = mActivity.getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(
+ mActivity.getBrowseTestSupportFragment().getSelectedPosition()).view;
+ PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.ViewStableOnScreen(row));
+ }
+
+ @Test
+ public void testTwoBackKeysWithBackStack() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ waitForHeaderTransitionFinished();
+ sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
+ }
+
+ @Test
+ public void testTwoBackKeysWithoutBackStack() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ waitForHeaderTransitionFinished();
+ sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
+ }
+
+ @Test
+ public void testPressRightBeforeMainFragmentCreated() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ assertNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
+ sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+ }
+
+ @Test
+ public void testSelectCardOnARow() throws Throwable {
+ final int selectRow = 10;
+ final int selectItem = 20;
+ Intent intent = new Intent();
+ final long dataLoadingDelay = 1000;
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ Presenter.ViewHolderTask itemTask = Mockito.spy(
+ new ItemSelectionTask(mActivity, selectRow));
+
+ final ListRowPresenter.SelectItemViewHolderTask task =
+ new ListRowPresenter.SelectItemViewHolderTask(selectItem);
+ task.setItemTask(itemTask);
+
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.getBrowseTestSupportFragment().setSelectedPosition(selectRow, true, task);
+ }
+ });
+
+ verify(itemTask, timeout(5000).times(1)).run(any(Presenter.ViewHolder.class));
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ListRowPresenter.ViewHolder row = (ListRowPresenter.ViewHolder) mActivity
+ .getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(selectRow);
+ assertNotNull(dumpRecyclerView(mActivity.getBrowseTestSupportFragment().getGridView()), row);
+ assertNotNull(row.getGridView());
+ assertEquals(selectItem, row.getGridView().getSelectedPosition());
+ }
+ });
+ }
+
+ @Test
+ public void activityRecreate_notCrash() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD, true);
+ mActivity = activityTestRule.launchActivity(intent);
+
+ waitForEntranceTransitionFinished();
+
+ InstrumentationRegistry.getInstrumentation().callActivityOnRestart(mActivity);
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.recreate();
+ }
+ });
+ }
+
+
+ @Test
+ public void lateLoadingHeaderDisabled() throws Throwable {
+ final long dataLoadingDelay = 1000;
+ Intent intent = new Intent();
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
+ intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE,
+ BrowseSupportFragment.HEADERS_DISABLED);
+ mActivity = activityTestRule.launchActivity(intent);
+ waitForEntranceTransitionFinished();
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mActivity.getBrowseTestSupportFragment().getGridView() != null
+ && mActivity.getBrowseTestSupportFragment().getGridView().getChildCount() > 0;
+ }
+ });
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ public static class ItemSelectionTask extends Presenter.ViewHolderTask {
+
+ private final BrowseSupportFragmentTestActivity activity;
+ private final int expectedRow;
+
+ public ItemSelectionTask(BrowseSupportFragmentTestActivity activity, int expectedRow) {
+ this.activity = activity;
+ this.expectedRow = expectedRow;
+ }
+
+ @Override
+ public void run(Presenter.ViewHolder holder) {
+ android.util.Log.d(TAG, dumpRecyclerView(activity.getBrowseTestSupportFragment()
+ .getGridView()));
+ android.util.Log.d(TAG, "Row " + expectedRow + " " + activity.getBrowseTestSupportFragment()
+ .getRowsSupportFragment().getRowViewHolder(expectedRow), new Exception());
+ }
+ }
+
+ static String dumpRecyclerView(RecyclerView recyclerView) {
+ StringBuffer b = new StringBuffer();
+ for (int i = 0; i < recyclerView.getChildCount(); i++) {
+ View child = recyclerView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ recyclerView.getChildViewHolder(child);
+ b.append("child").append(i).append(":").append(vh);
+ if (vh != null) {
+ b.append(",").append(vh.getViewHolder());
+ }
+ b.append(";");
+ }
+ return b.toString();
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
new file mode 100644
index 0000000..313dc86
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
@@ -0,0 +1,57 @@
+/*
+ * 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 android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+
+public class BrowseSupportFragmentTestActivity extends FragmentActivity {
+
+ public static final String EXTRA_ADD_TO_BACKSTACK = "addToBackStack";
+ public static final String EXTRA_NUM_ROWS = "numRows";
+ public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
+ public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
+ public static final String EXTRA_TEST_ENTRANCE_TRANSITION = "testEntranceTransition";
+ public static final String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
+ public static final String EXTRA_HEADERS_STATE = "headers_state";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ setContentView(R.layout.browse);
+ if (savedInstanceState == null) {
+ Bundle arguments = new Bundle();
+ arguments.putAll(intent.getExtras());
+ BrowseTestSupportFragment fragment = new BrowseTestSupportFragment();
+ fragment.setArguments(arguments);
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ if (intent.getBooleanExtra(EXTRA_ADD_TO_BACKSTACK, false)) {
+ ft.addToBackStack(null);
+ }
+ ft.commit();
+ }
+ }
+
+ public BrowseTestSupportFragment getBrowseTestSupportFragment() {
+ return (BrowseTestSupportFragment) getSupportFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
new file mode 100644
index 0000000..6a34c53
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
@@ -0,0 +1,175 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseTestSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_HEADERS_STATE;
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_NUM_ROWS;
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
+import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_TEST_ENTRANCE_TRANSITION;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.Log;
+import android.view.View;
+
+public class BrowseTestFragment extends BrowseFragment {
+ private static final String TAG = "BrowseTestFragment";
+
+ final static int DEFAULT_NUM_ROWS = 100;
+ final static int DEFAULT_REPEAT_PER_ROW = 20;
+ final static long DEFAULT_LOAD_DATA_DELAY = 2000;
+ final static boolean DEFAULT_TEST_ENTRANCE_TRANSITION = true;
+ final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
+
+ private ArrayObjectAdapter mRowsAdapter;
+
+ // For good performance, it's important to use a single instance of
+ // a card presenter for all rows using that presenter.
+ final static StringPresenter sCardPresenter = new StringPresenter();
+
+ int NUM_ROWS;
+ int REPEAT_PER_ROW;
+ boolean mEntranceTransitionStarted;
+ boolean mEntranceTransitionEnded;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ Bundle arguments = getArguments();
+ NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, BrowseTestFragment.DEFAULT_NUM_ROWS);
+ REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
+ DEFAULT_REPEAT_PER_ROW);
+ long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
+ DEFAULT_LOAD_DATA_DELAY);
+ boolean TEST_ENTRANCE_TRANSITION = arguments.getBoolean(
+ EXTRA_TEST_ENTRANCE_TRANSITION,
+ DEFAULT_TEST_ENTRANCE_TRANSITION);
+ final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
+ EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
+ DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
+
+ if (!SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+
+ setTitle("BrowseTestFragment");
+ setHeadersState(arguments.getInt(EXTRA_HEADERS_STATE, HEADERS_ENABLED));
+
+ setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Log.i(TAG, "onSearchClicked");
+ }
+ });
+
+ setOnItemViewClickedListener(new ItemViewClickedListener());
+ setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
+ + " " + rowViewHolder
+ + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
+ }
+ });
+ if (TEST_ENTRANCE_TRANSITION) {
+ // don't run entrance transition if fragment is restored.
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ }
+ // simulates in a real world use case data being loaded two seconds later
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isDestroyed()) {
+ return;
+ }
+ if (SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+ loadData();
+ startEntranceTransition();
+ }
+ }, LOAD_DATA_DELAY);
+ }
+
+ private void setupRows() {
+ ListRowPresenter lrp = new ListRowPresenter();
+
+ mRowsAdapter = new ArrayObjectAdapter(lrp);
+
+ setAdapter(mRowsAdapter);
+ }
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ super.onEntranceTransitionStart();
+ mEntranceTransitionStarted = true;
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ super.onEntranceTransitionEnd();
+ mEntranceTransitionEnded = true;
+ }
+
+ private void loadData() {
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < REPEAT_PER_ROW; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ private final class ItemViewClickedListener implements OnItemViewClickedListener {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ }
+
+ public VerticalGridView getGridView() {
+ return getRowsFragment().getVerticalGridView();
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
new file mode 100644
index 0000000..373d7a3
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
@@ -0,0 +1,172 @@
+/*
+ * 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 android.support.v17.leanback.app;
+
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE;
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_NUM_ROWS;
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
+import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_TEST_ENTRANCE_TRANSITION;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.Log;
+import android.view.View;
+
+public class BrowseTestSupportFragment extends BrowseSupportFragment {
+ private static final String TAG = "BrowseTestSupportFragment";
+
+ final static int DEFAULT_NUM_ROWS = 100;
+ final static int DEFAULT_REPEAT_PER_ROW = 20;
+ final static long DEFAULT_LOAD_DATA_DELAY = 2000;
+ final static boolean DEFAULT_TEST_ENTRANCE_TRANSITION = true;
+ final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
+
+ private ArrayObjectAdapter mRowsAdapter;
+
+ // For good performance, it's important to use a single instance of
+ // a card presenter for all rows using that presenter.
+ final static StringPresenter sCardPresenter = new StringPresenter();
+
+ int NUM_ROWS;
+ int REPEAT_PER_ROW;
+ boolean mEntranceTransitionStarted;
+ boolean mEntranceTransitionEnded;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ Bundle arguments = getArguments();
+ NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, BrowseTestSupportFragment.DEFAULT_NUM_ROWS);
+ REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
+ DEFAULT_REPEAT_PER_ROW);
+ long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
+ DEFAULT_LOAD_DATA_DELAY);
+ boolean TEST_ENTRANCE_TRANSITION = arguments.getBoolean(
+ EXTRA_TEST_ENTRANCE_TRANSITION,
+ DEFAULT_TEST_ENTRANCE_TRANSITION);
+ final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
+ EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
+ DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
+
+ if (!SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+
+ setTitle("BrowseTestSupportFragment");
+ setHeadersState(arguments.getInt(EXTRA_HEADERS_STATE, HEADERS_ENABLED));
+
+ setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Log.i(TAG, "onSearchClicked");
+ }
+ });
+
+ setOnItemViewClickedListener(new ItemViewClickedListener());
+ setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
+ + " " + rowViewHolder
+ + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
+ }
+ });
+ if (TEST_ENTRANCE_TRANSITION) {
+ // don't run entrance transition if fragment is restored.
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ }
+ // simulates in a real world use case data being loaded two seconds later
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isDestroyed()) {
+ return;
+ }
+ if (SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+ loadData();
+ startEntranceTransition();
+ }
+ }, LOAD_DATA_DELAY);
+ }
+
+ private void setupRows() {
+ ListRowPresenter lrp = new ListRowPresenter();
+
+ mRowsAdapter = new ArrayObjectAdapter(lrp);
+
+ setAdapter(mRowsAdapter);
+ }
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ super.onEntranceTransitionStart();
+ mEntranceTransitionStarted = true;
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ super.onEntranceTransitionEnd();
+ mEntranceTransitionEnded = true;
+ }
+
+ private void loadData() {
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < REPEAT_PER_ROW; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ private final class ItemViewClickedListener implements OnItemViewClickedListener {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ }
+
+ public VerticalGridView getGridView() {
+ return getRowsSupportFragment().getVerticalGridView();
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
new file mode 100644
index 0000000..bf70fae
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -0,0 +1,1219 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.widget.DetailsParallax;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+import android.support.v17.leanback.widget.RecyclerViewParallax;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link DetailsFragment}.
+ */
+@RunWith(JUnit4.class)
+@LargeTest
+public class DetailsFragmentTest extends SingleFragmentTestBase {
+
+ static final int PARALLAX_VERTICAL_OFFSET = -300;
+
+ static int getCoverDrawableAlpha(DetailsFragmentBackgroundController controller) {
+ return ((FitWidthBitmapDrawable) controller.mParallaxDrawable.getCoverDrawable())
+ .getAlpha();
+ }
+
+ public static class DetailsFragmentParallax extends DetailsTestFragment {
+
+ private DetailsParallaxDrawable mParallaxDrawable;
+
+ public DetailsFragmentParallax() {
+ super();
+ mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Drawable coverDrawable = new FitWidthBitmapDrawable();
+ mParallaxDrawable = new DetailsParallaxDrawable(
+ getActivity(),
+ getParallax(),
+ coverDrawable,
+ new ParallaxTarget.PropertyValuesHolderTarget(
+ coverDrawable,
+ PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
+ )
+ );
+
+ BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
+ backgroundManager.attach(getActivity().getWindow());
+ backgroundManager.setDrawable(mParallaxDrawable);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
+ }
+
+ DetailsParallaxDrawable getParallaxDrawable() {
+ return mParallaxDrawable;
+ }
+ }
+
+ @Test
+ public void parallaxSetupTest() {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentTest.DetailsFragmentParallax.class,
+ new SingleFragmentTestBase.Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ double delta = 0.0002;
+ DetailsParallax dpm = ((DetailsFragment) activity.getTestFragment()).getParallax();
+
+ RecyclerViewParallax.ChildPositionProperty frameTop =
+ (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
+ assertEquals(0f, frameTop.getFraction(), delta);
+ assertEquals(0f, frameTop.getAdapterPosition(), delta);
+
+
+ RecyclerViewParallax.ChildPositionProperty frameBottom =
+ (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
+ assertEquals(1f, frameBottom.getFraction(), delta);
+ assertEquals(0f, frameBottom.getAdapterPosition(), delta);
+ }
+
+ @Test
+ public void parallaxTest() throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(DetailsFragmentParallax.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsFragmentParallax detailsFragment =
+ (DetailsFragmentParallax) activity.getTestFragment();
+ DetailsParallaxDrawable drawable =
+ detailsFragment.getParallaxDrawable();
+ final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
+ drawable.getCoverDrawable();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsFragment().getAdapter() != null
+ && detailsFragment.getRowsFragment().getAdapter().size() > 1;
+ }
+ });
+
+ final VerticalGridView verticalGridView = detailsFragment.getRowsFragment()
+ .getVerticalGridView();
+ final int windowHeight = verticalGridView.getHeight();
+ final int windowWidth = verticalGridView.getWidth();
+ // make sure background manager attached to window is same size as VerticalGridView
+ // i.e. no status bar.
+ assertEquals(windowHeight, activity.getWindow().getDecorView().getHeight());
+ assertEquals(windowWidth, activity.getWindow().getDecorView().getWidth());
+
+ final View detailsFrame = verticalGridView.findViewById(R.id.details_frame);
+
+ assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+ final Rect detailsFrameRect = new Rect();
+ detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+ verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+ assertEquals(Math.min(windowHeight, detailsFrameRect.top),
+ bitmapDrawable.getBounds().height());
+ assertEquals(0, bitmapDrawable.getVerticalOffset());
+
+ assertTrue("TitleView is visible", detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ verticalGridView.scrollToPosition(1);
+ }
+ });
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
+ && detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
+ }
+ });
+
+ detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+ verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+ assertEquals(0, bitmapDrawable.getBounds().top);
+ assertEquals(Math.max(detailsFrameRect.top, 0), bitmapDrawable.getBounds().bottom);
+ assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+ ColorDrawable colorDrawable = (ColorDrawable) (drawable.getChildAt(1).getDrawable());
+ assertEquals(windowWidth, colorDrawable.getBounds().width());
+ assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
+ assertEquals(windowHeight, colorDrawable.getBounds().bottom);
+ }
+
+ public static class DetailsFragmentWithVideo extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+ MediaPlayerGlue mGlue;
+
+ public DetailsFragmentWithVideo() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mGlue = new MediaPlayerGlue(getActivity());
+ mDetailsBackground.setupVideoPlayback(mGlue);
+
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("A Googleer");
+ mGlue.setTitle("Diving with Sharks");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ public static class DetailsFragmentWithVideo1 extends DetailsFragmentWithVideo {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+
+ public static class DetailsFragmentWithVideo2 extends DetailsFragmentWithVideo {
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+
+ private void navigateBetweenRowsAndVideoUsingRequestFocusInternal(Class cls)
+ throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsFragmentWithVideo detailsFragment =
+ (DetailsFragmentWithVideo) activity.getTestFragment();
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mVideoFragment.getView().requestFocus();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+ assertFalse(detailsFragment.isShowingTitle());
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.getRowsFragment().getVerticalGridView().requestFocus();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() == originalFirstRowTop;
+ }
+ });
+ assertTrue(detailsFragment.isShowingTitle());
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingRequestFocus1() throws Throwable {
+ navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingRequestFocus2() throws Throwable {
+ navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsFragmentWithVideo2.class);
+ }
+
+ private void navigateBetweenRowsAndVideoUsingDPADInternal(Class cls) throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsFragmentWithVideo detailsFragment =
+ (DetailsFragmentWithVideo) activity.getTestFragment();
+ // wait video playing
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ // navigate to video
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+ }
+ });
+
+ // navigate to details
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() == originalFirstRowTop;
+ }
+ });
+ assertTrue(detailsFragment.isShowingTitle());
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingDPAD1() throws Throwable {
+ navigateBetweenRowsAndVideoUsingDPADInternal(DetailsFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingDPAD2() throws Throwable {
+ navigateBetweenRowsAndVideoUsingDPADInternal(DetailsFragmentWithVideo2.class);
+ }
+
+ public static class EmptyFragmentClass extends Fragment {
+ @Override
+ public void onStart() {
+ super.onStart();
+ getActivity().finish();
+ }
+ }
+
+ private void fragmentOnStartWithVideoInternal(Class cls) throws Throwable {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsFragmentWithVideo detailsFragment =
+ (DetailsFragmentWithVideo) activity.getTestFragment();
+ // wait video playing
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ // navigate to video
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+
+ // start an empty activity
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ Intent intent = new Intent(activity, SingleFragmentTestActivity.class);
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME,
+ EmptyFragmentClass.class.getName());
+ activity.startActivity(intent);
+ }
+ }
+ );
+ PollingCheck.waitFor(2000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.isResumed();
+ }
+ });
+ assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+ }
+
+ @Test
+ public void fragmentOnStartWithVideo1() throws Throwable {
+ fragmentOnStartWithVideoInternal(DetailsFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void fragmentOnStartWithVideo2() throws Throwable {
+ fragmentOnStartWithVideoInternal(DetailsFragmentWithVideo2.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndTitle() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsTestFragment.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsTestFragment detailsFragment =
+ (DetailsTestFragment) activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ }
+ });
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ });
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(detailsFragment.getTitleView().hasFocus());
+ assertEquals(originalFirstRowTop, firstRow.getTop());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.hasFocus());
+ assertEquals(originalFirstRowTop, firstRow.getTop());
+ }
+
+ public static class DetailsFragmentWithNoVideo extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentWithNoVideo() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void lateSetupVideo() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentWithNoVideo detailsFragment =
+ (DetailsFragmentWithNoVideo) activity.getTestFragment();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ assertTrue(firstRow.hasFocus());
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null;
+ }
+ });
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
+ .getPlaybackGlue()).isMediaPlaying();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+
+ // wait a little bit to replace with new Glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
+ glue2.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue2.setArtist("A Googleer");
+ glue2.setTitle("Diving with Sharks");
+ glue2.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // test switchToRows() and switchToVideo()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mRowsFragment.getView().hasFocus());
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ }
+
+ @Test
+ public void sharedGlueHost() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentWithNoVideo detailsFragment =
+ (DetailsFragmentWithNoVideo) activity.getTestFragment();
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue1 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue1);
+ glue1.setArtist("A Googleer");
+ glue1.setTitle("Diving with Sharks");
+ glue1.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null;
+ }
+ });
+
+ final MediaPlayerGlue glue1 = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+ PlaybackGlueHost playbackGlueHost = glue1.getHost();
+
+ // wait a little bit to replace with new Glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
+ glue2.setArtist("A Googleer");
+ glue2.setTitle("Diving with Sharks");
+ glue2.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // wait for new glue to get its glue host
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ MediaPlayerGlue mediaPlayerGlue = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+ return mediaPlayerGlue != null && mediaPlayerGlue != glue1
+ && mediaPlayerGlue.getHost() != null;
+ }
+ });
+
+ final MediaPlayerGlue glue2 = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+
+ assertTrue(glue1.getHost() == null);
+ assertTrue(glue2.getHost() == playbackGlueHost);
+ }
+
+ @Test
+ public void clearVideo() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentWithNoVideo detailsFragment =
+ (DetailsFragmentWithNoVideo) activity.getTestFragment();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
+ final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
+ .getPlaybackGlue()).isMediaPlaying();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+
+ // wait a little bit then reset glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(null);
+ }
+ }
+ );
+ // background should fade in upon reset playback
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 255 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
+ public static class DetailsFragmentWithNoItem extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentWithNoItem() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void noInitialItem() {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentWithNoItem.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentWithNoItem detailsFragment =
+ (DetailsFragmentWithNoItem) activity.getTestFragment();
+
+ final int recyclerViewHeight = detailsFragment.getRowsFragment().getVerticalGridView()
+ .getHeight();
+ assertTrue(recyclerViewHeight > 0);
+
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
+ assertEquals(0, coverDrawable.getBounds().top);
+ assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
+ Drawable bottomDrawable = detailsFragment.mDetailsBackgroundController.getBottomDrawable();
+ assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
+ assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
+ }
+
+ public static class DetailsFragmentSwitchToVideoInOnCreate extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentSwitchToVideoInOnCreate() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreate() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+ //SystemClock.sleep(5000);
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mVideoFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+ }
+ });
+
+ // switchToRows does nothing if there is no row
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+
+ // create item, it should be layout outside screen
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world",
+ "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+ );
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildCount() > 0
+ && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ >= detailsFragment.getVerticalGridView().getHeight();
+ }
+ });
+
+ // pressing BACK will return to details row
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+ }
+ });
+ assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+ }
+
+ @Test
+ public void switchToVideoBackToQuit() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mVideoFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+ }
+ });
+
+ // before any details row is presented, pressing BACK will quit the activity
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(activity));
+ }
+
+ public static class DetailsFragmentSwitchToVideoAndPrepareEntranceTransition
+ extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentSwitchToVideoAndPrepareEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ DetailsFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+ (DetailsFragmentSwitchToVideoAndPrepareEntranceTransition)
+ activity.getTestFragment();
+
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+ }
+
+ public static class DetailsFragmentEntranceTransition
+ extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void entranceTransitionBlocksSwitchToVideo() {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsFragmentEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentEntranceTransition detailsFragment =
+ (DetailsFragmentEntranceTransition)
+ activity.getTestFragment();
+
+ if (Build.VERSION.SDK_INT < 21) {
+ // when enter transition is not supported, mCanUseHost is immmediately true
+ assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ } else {
+ // calling switchToVideo() between prepareEntranceTransition and entrance transition
+ // finishes will be ignored.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ }
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ // once Entrance transition is finished, mCanUseHost will be true
+ // and we can switchToVideo and fade out the background.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
+ public static class DetailsFragmentEntranceTransitionTimeout extends DetailsTestFragment {
+
+ public DetailsFragmentEntranceTransitionTimeout() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ prepareEntranceTransition();
+ }
+
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void startEntranceTransitionAfterDestroyed() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ DetailsFragmentEntranceTransition.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN),
+ 1000);
+ final DetailsFragmentEntranceTransition detailsFragment =
+ (DetailsFragmentEntranceTransition)
+ activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ });
+ SystemClock.sleep(100);
+ activity.finish();
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
new file mode 100644
index 0000000..0178d26
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -0,0 +1,1216 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.widget.DetailsParallax;
+import android.support.v17.leanback.widget.DetailsParallaxDrawable;
+import android.support.v17.leanback.widget.ParallaxTarget;
+import android.support.v17.leanback.widget.RecyclerViewParallax;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link DetailsSupportFragment}.
+ */
+@RunWith(JUnit4.class)
+@LargeTest
+public class DetailsSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ static final int PARALLAX_VERTICAL_OFFSET = -300;
+
+ static int getCoverDrawableAlpha(DetailsSupportFragmentBackgroundController controller) {
+ return ((FitWidthBitmapDrawable) controller.mParallaxDrawable.getCoverDrawable())
+ .getAlpha();
+ }
+
+ public static class DetailsSupportFragmentParallax extends DetailsTestSupportFragment {
+
+ private DetailsParallaxDrawable mParallaxDrawable;
+
+ public DetailsSupportFragmentParallax() {
+ super();
+ mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Drawable coverDrawable = new FitWidthBitmapDrawable();
+ mParallaxDrawable = new DetailsParallaxDrawable(
+ getActivity(),
+ getParallax(),
+ coverDrawable,
+ new ParallaxTarget.PropertyValuesHolderTarget(
+ coverDrawable,
+ PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
+ )
+ );
+
+ BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
+ backgroundManager.attach(getActivity().getWindow());
+ backgroundManager.setDrawable(mParallaxDrawable);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
+ }
+
+ DetailsParallaxDrawable getParallaxDrawable() {
+ return mParallaxDrawable;
+ }
+ }
+
+ @Test
+ public void parallaxSetupTest() {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentTest.DetailsSupportFragmentParallax.class,
+ new SingleSupportFragmentTestBase.Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ double delta = 0.0002;
+ DetailsParallax dpm = ((DetailsSupportFragment) activity.getTestFragment()).getParallax();
+
+ RecyclerViewParallax.ChildPositionProperty frameTop =
+ (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
+ assertEquals(0f, frameTop.getFraction(), delta);
+ assertEquals(0f, frameTop.getAdapterPosition(), delta);
+
+
+ RecyclerViewParallax.ChildPositionProperty frameBottom =
+ (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
+ assertEquals(1f, frameBottom.getFraction(), delta);
+ assertEquals(0f, frameBottom.getAdapterPosition(), delta);
+ }
+
+ @Test
+ public void parallaxTest() throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(DetailsSupportFragmentParallax.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsSupportFragmentParallax detailsFragment =
+ (DetailsSupportFragmentParallax) activity.getTestFragment();
+ DetailsParallaxDrawable drawable =
+ detailsFragment.getParallaxDrawable();
+ final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
+ drawable.getCoverDrawable();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsSupportFragment().getAdapter() != null
+ && detailsFragment.getRowsSupportFragment().getAdapter().size() > 1;
+ }
+ });
+
+ final VerticalGridView verticalGridView = detailsFragment.getRowsSupportFragment()
+ .getVerticalGridView();
+ final int windowHeight = verticalGridView.getHeight();
+ final int windowWidth = verticalGridView.getWidth();
+ // make sure background manager attached to window is same size as VerticalGridView
+ // i.e. no status bar.
+ assertEquals(windowHeight, activity.getWindow().getDecorView().getHeight());
+ assertEquals(windowWidth, activity.getWindow().getDecorView().getWidth());
+
+ final View detailsFrame = verticalGridView.findViewById(R.id.details_frame);
+
+ assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+ final Rect detailsFrameRect = new Rect();
+ detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+ verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+ assertEquals(Math.min(windowHeight, detailsFrameRect.top),
+ bitmapDrawable.getBounds().height());
+ assertEquals(0, bitmapDrawable.getVerticalOffset());
+
+ assertTrue("TitleView is visible", detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ verticalGridView.scrollToPosition(1);
+ }
+ });
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
+ && detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
+ }
+ });
+
+ detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
+ verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
+
+ assertEquals(0, bitmapDrawable.getBounds().top);
+ assertEquals(Math.max(detailsFrameRect.top, 0), bitmapDrawable.getBounds().bottom);
+ assertEquals(windowWidth, bitmapDrawable.getBounds().width());
+
+ ColorDrawable colorDrawable = (ColorDrawable) (drawable.getChildAt(1).getDrawable());
+ assertEquals(windowWidth, colorDrawable.getBounds().width());
+ assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
+ assertEquals(windowHeight, colorDrawable.getBounds().bottom);
+ }
+
+ public static class DetailsSupportFragmentWithVideo extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+ MediaPlayerGlue mGlue;
+
+ public DetailsSupportFragmentWithVideo() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mGlue = new MediaPlayerGlue(getActivity());
+ mDetailsBackground.setupVideoPlayback(mGlue);
+
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("A Googleer");
+ mGlue.setTitle("Diving with Sharks");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ public static class DetailsSupportFragmentWithVideo1 extends DetailsSupportFragmentWithVideo {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+
+ public static class DetailsSupportFragmentWithVideo2 extends DetailsSupportFragmentWithVideo {
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+
+ private void navigateBetweenRowsAndVideoUsingRequestFocusInternal(Class cls)
+ throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsSupportFragmentWithVideo detailsFragment =
+ (DetailsSupportFragmentWithVideo) activity.getTestFragment();
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mVideoSupportFragment.getView().requestFocus();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+ assertFalse(detailsFragment.isShowingTitle());
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.getRowsSupportFragment().getVerticalGridView().requestFocus();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() == originalFirstRowTop;
+ }
+ });
+ assertTrue(detailsFragment.isShowingTitle());
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingRequestFocus1() throws Throwable {
+ navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsSupportFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingRequestFocus2() throws Throwable {
+ navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsSupportFragmentWithVideo2.class);
+ }
+
+ private void navigateBetweenRowsAndVideoUsingDPADInternal(Class cls) throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsSupportFragmentWithVideo detailsFragment =
+ (DetailsSupportFragmentWithVideo) activity.getTestFragment();
+ // wait video playing
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ // navigate to video
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+ }
+ });
+
+ // navigate to details
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() == originalFirstRowTop;
+ }
+ });
+ assertTrue(detailsFragment.isShowingTitle());
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingDPAD1() throws Throwable {
+ navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndVideoUsingDPAD2() throws Throwable {
+ navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo2.class);
+ }
+
+ public static class EmptyFragmentClass extends Fragment {
+ @Override
+ public void onStart() {
+ super.onStart();
+ getActivity().finish();
+ }
+ }
+
+ private void fragmentOnStartWithVideoInternal(Class cls) throws Throwable {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+
+ final DetailsSupportFragmentWithVideo detailsFragment =
+ (DetailsSupportFragmentWithVideo) activity.getTestFragment();
+ // wait video playing
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ assertTrue(firstRow.hasFocus());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+ assertTrue(detailsFragment.isShowingTitle());
+
+ // navigate to video
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return firstRow.getTop() >= screenHeight;
+ }
+ });
+
+ // start an empty activity
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ Intent intent = new Intent(activity, SingleSupportFragmentTestActivity.class);
+ intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_FRAGMENT_NAME,
+ EmptyFragmentClass.class.getName());
+ activity.startActivity(intent);
+ }
+ }
+ );
+ PollingCheck.waitFor(2000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.isResumed();
+ }
+ });
+ assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+ }
+
+ @Test
+ public void fragmentOnStartWithVideo1() throws Throwable {
+ fragmentOnStartWithVideoInternal(DetailsSupportFragmentWithVideo1.class);
+ }
+
+ @Test
+ public void fragmentOnStartWithVideo2() throws Throwable {
+ fragmentOnStartWithVideoInternal(DetailsSupportFragmentWithVideo2.class);
+ }
+
+ @Test
+ public void navigateBetweenRowsAndTitle() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsTestSupportFragment.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsTestSupportFragment detailsFragment =
+ (DetailsTestSupportFragment) activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ }
+ });
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ });
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int originalFirstRowTop = firstRow.getTop();
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(detailsFragment.getTitleView().hasFocus());
+ assertEquals(originalFirstRowTop, firstRow.getTop());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.hasFocus());
+ assertEquals(originalFirstRowTop, firstRow.getTop());
+ }
+
+ public static class DetailsSupportFragmentWithNoVideo extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentWithNoVideo() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+
+ setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void lateSetupVideo() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentWithNoVideo detailsFragment =
+ (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ assertTrue(firstRow.hasFocus());
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null;
+ }
+ });
+ sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+ assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
+ .getPlaybackGlue()).isMediaPlaying();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+
+ // wait a little bit to replace with new Glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
+ glue2.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue2.setArtist("A Googleer");
+ glue2.setTitle("Diving with Sharks");
+ glue2.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // test switchToRows() and switchToVideo()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mRowsSupportFragment.getView().hasFocus());
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
+ }
+
+ @Test
+ public void sharedGlueHost() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentWithNoVideo detailsFragment =
+ (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue1 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue1);
+ glue1.setArtist("A Googleer");
+ glue1.setTitle("Diving with Sharks");
+ glue1.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null;
+ }
+ });
+
+ final MediaPlayerGlue glue1 = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+ PlaybackGlueHost playbackGlueHost = glue1.getHost();
+
+ // wait a little bit to replace with new Glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
+ glue2.setArtist("A Googleer");
+ glue2.setTitle("Diving with Sharks");
+ glue2.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ // wait for new glue to get its glue host
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ MediaPlayerGlue mediaPlayerGlue = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+ return mediaPlayerGlue != null && mediaPlayerGlue != glue1
+ && mediaPlayerGlue.getHost() != null;
+ }
+ });
+
+ final MediaPlayerGlue glue2 = (MediaPlayerGlue) detailsFragment
+ .mDetailsBackgroundController
+ .getPlaybackGlue();
+
+ assertTrue(glue1.getHost() == null);
+ assertTrue(glue2.getHost() == playbackGlueHost);
+ }
+
+ @Test
+ public void clearVideo() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentWithNoVideo detailsFragment =
+ (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
+ }
+ });
+ final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
+ final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+
+ assertTrue(firstRow.hasFocus());
+ assertTrue(detailsFragment.isShowingTitle());
+ assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
+
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
+ .getPlaybackGlue()).isMediaPlaying();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+
+ // wait a little bit then reset glue
+ SystemClock.sleep(1000);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(null);
+ }
+ }
+ );
+ // background should fade in upon reset playback
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 255 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
+ public static class DetailsSupportFragmentWithNoItem extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentWithNoItem() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void noInitialItem() {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentWithNoItem.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentWithNoItem detailsFragment =
+ (DetailsSupportFragmentWithNoItem) activity.getTestFragment();
+
+ final int recyclerViewHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
+ .getHeight();
+ assertTrue(recyclerViewHeight > 0);
+
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
+ assertEquals(0, coverDrawable.getBounds().top);
+ assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
+ Drawable bottomDrawable = detailsFragment.mDetailsBackgroundController.getBottomDrawable();
+ assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
+ assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
+ }
+
+ public static class DetailsSupportFragmentSwitchToVideoInOnCreate extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentSwitchToVideoInOnCreate() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreate() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+ //SystemClock.sleep(5000);
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoSupportFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+ }
+ });
+
+ // switchToRows does nothing if there is no row
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+
+ // create item, it should be layout outside screen
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world",
+ "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+ );
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildCount() > 0
+ && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ >= detailsFragment.getVerticalGridView().getHeight();
+ }
+ });
+
+ // pressing BACK will return to details row
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+ }
+ });
+ assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+ }
+
+ @Test
+ public void switchToVideoBackToQuit() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoSupportFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+ }
+ });
+
+ // before any details row is presented, pressing BACK will quit the activity
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(activity));
+ }
+
+ public static class DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition
+ extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition)
+ activity.getTestFragment();
+
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+ }
+
+ public static class DetailsSupportFragmentEntranceTransition
+ extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void entranceTransitionBlocksSwitchToVideo() {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(DetailsSupportFragmentEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentEntranceTransition detailsFragment =
+ (DetailsSupportFragmentEntranceTransition)
+ activity.getTestFragment();
+
+ if (Build.VERSION.SDK_INT < 21) {
+ // when enter transition is not supported, mCanUseHost is immmediately true
+ assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ } else {
+ // calling switchToVideo() between prepareEntranceTransition and entrance transition
+ // finishes will be ignored.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ }
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ // once Entrance transition is finished, mCanUseHost will be true
+ // and we can switchToVideo and fade out the background.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
+ public static class DetailsSupportFragmentEntranceTransitionTimeout extends DetailsTestSupportFragment {
+
+ public DetailsSupportFragmentEntranceTransitionTimeout() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ prepareEntranceTransition();
+ }
+
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void startEntranceTransitionAfterDestroyed() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ DetailsSupportFragmentEntranceTransition.class, new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN),
+ 1000);
+ final DetailsSupportFragmentEntranceTransition detailsFragment =
+ (DetailsSupportFragmentEntranceTransition)
+ activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ });
+ SystemClock.sleep(100);
+ activity.finish();
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
new file mode 100644
index 0000000..833b344
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
@@ -0,0 +1,148 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsTestSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.ViewGroup;
+
+/**
+ * Base class provides overview row and some related rows.
+ */
+public class DetailsTestFragment extends android.support.v17.leanback.app.DetailsFragment {
+ private static final int NUM_ROWS = 3;
+ private ArrayObjectAdapter mRowsAdapter;
+ private PhotoItem mPhotoItem;
+ private final Presenter mCardPresenter = new Presenter() {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ ImageCardView cardView = new ImageCardView(getActivity());
+ cardView.setFocusable(true);
+ cardView.setFocusableInTouchMode(true);
+ return new ViewHolder(cardView);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object item) {
+ ImageCardView imageCardView = (ImageCardView) viewHolder.view;
+ imageCardView.setTitleText("Android Tv");
+ imageCardView.setContentText("Android Tv Production Inc.");
+ imageCardView.setMainImageDimensions(313, 176);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ }
+ };
+
+ private static final int ACTION_RENT = 2;
+ private static final int ACTION_BUY = 3;
+
+ protected long mTimeToLoadOverviewRow = 1000;
+ protected long mTimeToLoadRelatedRow = 2000;
+
+ private Action mActionRent;
+ private Action mActionBuy;
+
+ protected int mMinVerticalOffset = -100;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle("Leanback Sample App");
+
+ mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
+ getResources().getDrawable(R.drawable.ic_action_a));
+ mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
+
+ ClassPresenterSelector ps = new ClassPresenterSelector();
+ FullWidthDetailsOverviewRowPresenter dorPresenter =
+ new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
+ @Override
+ protected void onBindDescription(
+ AbstractDetailsDescriptionPresenter.ViewHolder vh, Object item) {
+ vh.getTitle().setText("Funny Movie");
+ vh.getSubtitle().setText("Android TV Production Inc.");
+ vh.getBody().setText("What a great movie!");
+ }
+ });
+
+ ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
+ ps.addClassPresenter(ListRow.class, new ListRowPresenter());
+ mRowsAdapter = new ArrayObjectAdapter(ps);
+ }
+
+ public void setItem(PhotoItem photoItem) {
+ mPhotoItem = photoItem;
+ mRowsAdapter.clear();
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null) {
+ return;
+ }
+ Resources res = getActivity().getResources();
+ DetailsOverviewRow dor = new DetailsOverviewRow(mPhotoItem.getTitle());
+ dor.setImageDrawable(res.getDrawable(mPhotoItem.getImageResourceId()));
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();
+ adapter.set(ACTION_RENT, mActionRent);
+ adapter.set(ACTION_BUY, mActionBuy);
+ dor.setActionsAdapter(adapter);
+ mRowsAdapter.add(0, dor);
+ setSelectedPosition(0, true);
+ }
+ }, mTimeToLoadOverviewRow);
+
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null) {
+ return;
+ }
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(mCardPresenter);
+ listRowAdapter.add(new PhotoItem("Hello world", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("This is a test", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("Android TV", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("Leanback", R.drawable.spiderman));
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+ }, mTimeToLoadRelatedRow);
+
+ setAdapter(mRowsAdapter);
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
new file mode 100644
index 0000000..e0d60b4
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ImageCardView;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.ViewGroup;
+
+/**
+ * Base class provides overview row and some related rows.
+ */
+public class DetailsTestSupportFragment extends android.support.v17.leanback.app.DetailsSupportFragment {
+ private static final int NUM_ROWS = 3;
+ private ArrayObjectAdapter mRowsAdapter;
+ private PhotoItem mPhotoItem;
+ private final Presenter mCardPresenter = new Presenter() {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ ImageCardView cardView = new ImageCardView(getActivity());
+ cardView.setFocusable(true);
+ cardView.setFocusableInTouchMode(true);
+ return new ViewHolder(cardView);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object item) {
+ ImageCardView imageCardView = (ImageCardView) viewHolder.view;
+ imageCardView.setTitleText("Android Tv");
+ imageCardView.setContentText("Android Tv Production Inc.");
+ imageCardView.setMainImageDimensions(313, 176);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ }
+ };
+
+ private static final int ACTION_RENT = 2;
+ private static final int ACTION_BUY = 3;
+
+ protected long mTimeToLoadOverviewRow = 1000;
+ protected long mTimeToLoadRelatedRow = 2000;
+
+ private Action mActionRent;
+ private Action mActionBuy;
+
+ protected int mMinVerticalOffset = -100;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle("Leanback Sample App");
+
+ mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
+ getResources().getDrawable(R.drawable.ic_action_a));
+ mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
+
+ ClassPresenterSelector ps = new ClassPresenterSelector();
+ FullWidthDetailsOverviewRowPresenter dorPresenter =
+ new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
+ @Override
+ protected void onBindDescription(
+ AbstractDetailsDescriptionPresenter.ViewHolder vh, Object item) {
+ vh.getTitle().setText("Funny Movie");
+ vh.getSubtitle().setText("Android TV Production Inc.");
+ vh.getBody().setText("What a great movie!");
+ }
+ });
+
+ ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
+ ps.addClassPresenter(ListRow.class, new ListRowPresenter());
+ mRowsAdapter = new ArrayObjectAdapter(ps);
+ }
+
+ public void setItem(PhotoItem photoItem) {
+ mPhotoItem = photoItem;
+ mRowsAdapter.clear();
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null) {
+ return;
+ }
+ Resources res = getActivity().getResources();
+ DetailsOverviewRow dor = new DetailsOverviewRow(mPhotoItem.getTitle());
+ dor.setImageDrawable(res.getDrawable(mPhotoItem.getImageResourceId()));
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();
+ adapter.set(ACTION_RENT, mActionRent);
+ adapter.set(ACTION_BUY, mActionBuy);
+ dor.setActionsAdapter(adapter);
+ mRowsAdapter.add(0, dor);
+ setSelectedPosition(0, true);
+ }
+ }, mTimeToLoadOverviewRow);
+
+
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null) {
+ return;
+ }
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(mCardPresenter);
+ listRowAdapter.add(new PhotoItem("Hello world", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("This is a test", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("Android TV", R.drawable.spiderman));
+ listRowAdapter.add(new PhotoItem("Leanback", R.drawable.spiderman));
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+ }, mTimeToLoadRelatedRow);
+
+ setAdapter(mRowsAdapter);
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
new file mode 100644
index 0000000..650391d
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
@@ -0,0 +1,505 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class GuidedStepFragmentTest extends GuidedStepFragmentTestBase {
+
+ private static final int ON_DESTROY_TIMEOUT = 5000;
+
+ @Test
+ public void nextAndBack() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1000) {
+ GuidedStepFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestFragment(secondFragmentName));
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
+
+ GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+ verify(first, times(1)).onCreate(nullable(Bundle.class));
+ verify(first, times(1)).onCreateGuidance(nullable(Bundle.class));
+ verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(1)).onViewStateRestored(nullable(Bundle.class));
+ verify(first, times(1)).onStart();
+ verify(first, times(1)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(first, times(1)).onGuidedActionClicked(any(GuidedAction.class));
+
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(first, times(1)).onPause();
+ verify(first, times(1)).onStop();
+ verify(first, times(1)).onDestroyView();
+ verify(second, times(1)).onCreate(nullable(Bundle.class));
+ verify(second, times(1)).onCreateGuidance(nullable(Bundle.class));
+ verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+ verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(second, times(1)).onViewStateRestored(nullable(Bundle.class));
+ verify(second, times(1)).onStart();
+ verify(second, times(1)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(second, times(1)).onPause();
+ verify(second, times(1)).onStop();
+ verify(second, times(1)).onDestroyView();
+ verify(second, times(1)).onDestroy();
+ verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(2)).onViewStateRestored(nullable(Bundle.class));
+ verify(first, times(2)).onStart();
+ verify(first, times(2)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ assertTrue(activity.isDestroyed());
+ }
+
+ @Test
+ public void restoreFragments() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
+ actions.add(new GuidedAction.Builder().id(1001).editable(true).title("text")
+ .build());
+ actions.add(new GuidedAction.Builder().id(1002).editable(true).title("text")
+ .autoSaveRestoreEnabled(false).build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1000) {
+ GuidedStepFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestFragment(secondFragmentName));
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+ first.getFragment().findActionById(1001).setTitle("modified text");
+ first.getFragment().findActionById(1002).setTitle("modified text");
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(1)).onDestroy();
+ verify(second, times(2)).onCreate(nullable(Bundle.class));
+ verify(second, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(second, times(1)).onDestroy();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(second, times(2)).onPause();
+ verify(second, times(2)).onStop();
+ verify(second, times(2)).onDestroyView();
+ verify(second, times(2)).onDestroy();
+ assertEquals("modified text", first.getFragment().findActionById(1001).getTitle());
+ assertEquals("text", first.getFragment().findActionById(1002).getTitle());
+ verify(first, times(2)).onCreate(nullable(Bundle.class));
+ verify(first, times(2)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ }
+
+
+ @Test
+ public void finishGuidedStepFragment_finishes_activity() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("Finish activity").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1001) {
+ obj.getFragment().finishGuidedStepFragments();
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ View viewFinish = first.getFragment().getActionItemView(0);
+ assertTrue(viewFinish.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ }
+
+ @Test
+ public void finishGuidedStepFragment_finishes_fragments() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("Finish fragments").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1001) {
+ obj.getFragment().finishGuidedStepFragments();
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ false /*asRoot*/);
+
+ View viewFinish = first.getFragment().getActionItemView(0);
+ assertTrue(viewFinish.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+
+ // fragment should be destroyed, activity should not destroyed
+ waitOnDestroy(first, 1);
+ assertFalse(activity.isDestroyed());
+ }
+
+ @Test
+ public void subActions() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ final boolean[] expandSubActionInOnCreateView = new boolean[] {false};
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ if (expandSubActionInOnCreateView[0]) {
+ obj.getFragment().expandAction(obj.getFragment().findActionById(1000), false);
+ }
+ return null;
+ }
+ }).when(first).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+ subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+ subActions.add(new GuidedAction.Builder().id(2001).title("item2").build());
+ actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+ .title("list").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) {
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ if (action.getId() == 2000) {
+ return true;
+ } else if (action.getId() == 2001) {
+ GuidedStepFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestFragment(secondFragmentName));
+ return false;
+ }
+ return false;
+ }
+ }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ // after clicked, it sub actions list should expand
+ View viewForList = first.getFragment().getActionItemView(0);
+ assertTrue(viewForList.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(viewForList.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+ assertEquals(2000, actionCapture.getValue().getId());
+ // after clicked a sub action, it sub actions list should close
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertTrue(viewForList.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+
+ assertFalse(viewForList.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture2 = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(2)).onSubGuidedActionClicked(actionCapture2.capture());
+ assertEquals(2001, actionCapture2.getValue().getId());
+
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+
+ // test expand sub action when return to first fragment
+ expandSubActionInOnCreateView[0] = true;
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ assertTrue(first.getFragment().isExpanded());
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(first.getFragment().isExpanded());
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ }
+
+ @Test
+ public void setActionsWhenSubActionsExpanded() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+ subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+ actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+ .title("list").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) {
+ GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+ invocation.getMock();
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ if (action.getId() == 2000) {
+ List<GuidedAction> newActions = new ArrayList<GuidedAction>();
+ newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
+ obj.getFragment().setActions(newActions);
+ return false;
+ }
+ return false;
+ }
+ }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ // after clicked, it sub actions list should expand
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(firstView.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+ // after clicked a sub action, whole action list is replaced.
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(first.getFragment().isExpanded());
+ View newFirstView = first.getFragment().getActionItemView(0);
+ assertTrue(newFirstView.hasFocus());
+ assertTrue(newFirstView.getVisibility() == View.VISIBLE);
+ GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
+ .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
+ assertEquals(1001, vh.getAction().getId());
+
+ }
+
+ @Test
+ public void buttonActionsRtl() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
+ return null;
+ }
+ }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ true, View.LAYOUT_DIRECTION_RTL);
+
+ assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ }
+
+ @Test
+ public void recyclerViewDiffTest() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action1").build());
+ actions.add(new GuidedAction.Builder().id(1001).title("action2").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+
+ launchTestActivity(firstFragmentName, true);
+
+ final ArrayList<RecyclerView.ViewHolder> changeList = new ArrayList();
+ VerticalGridView rv = first.getFragment().mActionsStylist.getActionsGridView();
+ rv.setItemAnimator(new DefaultItemAnimator() {
+ @Override
+ public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
+ if (!oldItem) {
+ changeList.add(item);
+ }
+ super.onChangeStarting(item, oldItem);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ List actions = new ArrayList();
+ actions.add(new GuidedAction.Builder().id(1001).title("action2x").build());
+ actions.add(new GuidedAction.Builder().id(1000).title("action1x").build());
+ first.getFragment().setActions(actions);
+ }
+ });
+
+ // should causes two change animation.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return changeList.size() == 2;
+ }
+ });
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
new file mode 100644
index 0000000..dd17fd3
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
@@ -0,0 +1,66 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFragmentTestActivity.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.app.Activity;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepFragmentTestActivity extends Activity {
+
+ /**
+ * Frst Test that will be included in this Activity
+ */
+ public static final String EXTRA_TEST_NAME = "testName";
+ /**
+ * True(default) to addAsRoot() for first Test, false to use add()
+ */
+ public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
+
+ /**
+ * Layout direction
+ */
+ public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+
+ int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
+ if (layoutDirection != -1) {
+ findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
+ }
+ if (savedInstanceState == null) {
+ String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
+ if (firstTestName != null) {
+ GuidedStepTestFragment testFragment = new GuidedStepTestFragment(firstTestName);
+ if (intent.getBooleanExtra(EXTRA_ADD_AS_ROOT, true)) {
+ GuidedStepTestFragment.addAsRoot(this, testFragment, android.R.id.content);
+ } else {
+ GuidedStepTestFragment.add(getFragmentManager(), testFragment,
+ android.R.id.content);
+ }
+ }
+ }
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
new file mode 100644
index 0000000..34ec694
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
@@ -0,0 +1,149 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFrgamentTestBase.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepFragmentTestBase {
+
+ private static final long TIMEOUT = 5000;
+
+ @Rule public TestName mUnitTestName = new TestName();
+
+ @Rule
+ public ActivityTestRule<GuidedStepFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(GuidedStepFragmentTestActivity.class, false, false);
+
+ @Before
+ public void clearTests() {
+ GuidedStepTestFragment.clearTests();
+ }
+
+ public static class ExpandTransitionFinish extends PollingCheck.PollingCheckCondition {
+ GuidedStepTestFragment.Provider mProvider;
+
+ public ExpandTransitionFinish(GuidedStepTestFragment.Provider provider) {
+ mProvider = provider;
+ }
+
+ @Override
+ public boolean canPreProceed() {
+ return false;
+ }
+
+ @Override
+ public boolean canProceed() {
+ GuidedStepTestFragment fragment = mProvider.getFragment();
+ if (fragment != null && fragment.getView() != null) {
+ if (!fragment.getGuidedActionsStylist().isInExpandTransition()) {
+ // expand transition finishes
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public static void waitOnDestroy(GuidedStepTestFragment.Provider provider,
+ int times) {
+ verify(provider, timeout((int)TIMEOUT).times(times)).onDestroy();
+ }
+
+ public static class EnterTransitionFinish extends PollingCheck.PollingCheckCondition {
+ PollingCheck.ViewScreenPositionDetector mDector =
+ new PollingCheck.ViewScreenPositionDetector();
+
+ GuidedStepTestFragment.Provider mProvider;
+
+ public EnterTransitionFinish(GuidedStepTestFragment.Provider provider) {
+ mProvider = provider;
+ }
+ @Override
+ public boolean canProceed() {
+ GuidedStepTestFragment fragment = mProvider.getFragment();
+ if (fragment != null && fragment.getView() != null) {
+ View view = fragment.getView().findViewById(R.id.guidance_title);
+ if (view != null) {
+ if (mDector.isViewStableOnScreen(view)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ public static void sendKey(int keyCode) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
+ }
+
+ public String generateMethodTestName(String testName) {
+ return mUnitTestName.getMethodName() + "_" + testName;
+ }
+
+ public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot, int layoutDirection) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepTestFragment.Provider mockProvider(String testName) {
+ GuidedStepTestFragment.Provider test = mock(GuidedStepTestFragment.Provider.class);
+ when(test.getActivity()).thenCallRealMethod();
+ when(test.getFragmentManager()).thenCallRealMethod();
+ when(test.getFragment()).thenCallRealMethod();
+ GuidedStepTestFragment.setupTest(testName, test);
+ return test;
+ }
+}
+
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
new file mode 100644
index 0000000..5f015a1
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class GuidedStepSupportFragmentTest extends GuidedStepSupportFragmentTestBase {
+
+ private static final int ON_DESTROY_TIMEOUT = 5000;
+
+ @Test
+ public void nextAndBack() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1000) {
+ GuidedStepSupportFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestSupportFragment(secondFragmentName));
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
+
+ GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+ verify(first, times(1)).onCreate(nullable(Bundle.class));
+ verify(first, times(1)).onCreateGuidance(nullable(Bundle.class));
+ verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(1)).onViewStateRestored(nullable(Bundle.class));
+ verify(first, times(1)).onStart();
+ verify(first, times(1)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(first, times(1)).onGuidedActionClicked(any(GuidedAction.class));
+
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(first, times(1)).onPause();
+ verify(first, times(1)).onStop();
+ verify(first, times(1)).onDestroyView();
+ verify(second, times(1)).onCreate(nullable(Bundle.class));
+ verify(second, times(1)).onCreateGuidance(nullable(Bundle.class));
+ verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+ verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(second, times(1)).onViewStateRestored(nullable(Bundle.class));
+ verify(second, times(1)).onStart();
+ verify(second, times(1)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(second, times(1)).onPause();
+ verify(second, times(1)).onStop();
+ verify(second, times(1)).onDestroyView();
+ verify(second, times(1)).onDestroy();
+ verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(2)).onViewStateRestored(nullable(Bundle.class));
+ verify(first, times(2)).onStart();
+ verify(first, times(2)).onResume();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ assertTrue(activity.isDestroyed());
+ }
+
+ @Test
+ public void restoreFragments() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
+ actions.add(new GuidedAction.Builder().id(1001).editable(true).title("text")
+ .build());
+ actions.add(new GuidedAction.Builder().id(1002).editable(true).title("text")
+ .autoSaveRestoreEnabled(false).build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1000) {
+ GuidedStepSupportFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestSupportFragment(secondFragmentName));
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+ first.getFragment().findActionById(1001).setTitle("modified text");
+ first.getFragment().findActionById(1002).setTitle("modified text");
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(first, times(1)).onDestroy();
+ verify(second, times(2)).onCreate(nullable(Bundle.class));
+ verify(second, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ verify(second, times(1)).onDestroy();
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(second, times(2)).onPause();
+ verify(second, times(2)).onStop();
+ verify(second, times(2)).onDestroyView();
+ verify(second, times(2)).onDestroy();
+ assertEquals("modified text", first.getFragment().findActionById(1001).getTitle());
+ assertEquals("text", first.getFragment().findActionById(1002).getTitle());
+ verify(first, times(2)).onCreate(nullable(Bundle.class));
+ verify(first, times(2)).onCreateActions(any(List.class), nullable(Bundle.class));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ }
+
+
+ @Test
+ public void finishGuidedStepSupportFragment_finishes_activity() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("Finish activity").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1001) {
+ obj.getFragment().finishGuidedStepSupportFragments();
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ View viewFinish = first.getFragment().getActionItemView(0);
+ assertTrue(viewFinish.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ }
+
+ @Test
+ public void finishGuidedStepSupportFragment_finishes_fragments() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("Finish fragments").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ if (action.getId() == 1001) {
+ obj.getFragment().finishGuidedStepSupportFragments();
+ }
+ return null;
+ }
+ }).when(first).onGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ false /*asRoot*/);
+
+ View viewFinish = first.getFragment().getActionItemView(0);
+ assertTrue(viewFinish.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+
+ // fragment should be destroyed, activity should not destroyed
+ waitOnDestroy(first, 1);
+ assertFalse(activity.isDestroyed());
+ }
+
+ @Test
+ public void subActions() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final String secondFragmentName = generateMethodTestName("second");
+ final boolean[] expandSubActionInOnCreateView = new boolean[] {false};
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ if (expandSubActionInOnCreateView[0]) {
+ obj.getFragment().expandAction(obj.getFragment().findActionById(1000), false);
+ }
+ return null;
+ }
+ }).when(first).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+ subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+ subActions.add(new GuidedAction.Builder().id(2001).title("item2").build());
+ actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+ .title("list").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) {
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ if (action.getId() == 2000) {
+ return true;
+ } else if (action.getId() == 2001) {
+ GuidedStepSupportFragment.add(obj.getFragmentManager(),
+ new GuidedStepTestSupportFragment(secondFragmentName));
+ return false;
+ }
+ return false;
+ }
+ }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+ GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ // after clicked, it sub actions list should expand
+ View viewForList = first.getFragment().getActionItemView(0);
+ assertTrue(viewForList.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(viewForList.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+ assertEquals(2000, actionCapture.getValue().getId());
+ // after clicked a sub action, it sub actions list should close
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertTrue(viewForList.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+
+ assertFalse(viewForList.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture2 = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(2)).onSubGuidedActionClicked(actionCapture2.capture());
+ assertEquals(2001, actionCapture2.getValue().getId());
+
+ PollingCheck.waitFor(new EnterTransitionFinish(second));
+ verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+
+ // test expand sub action when return to first fragment
+ expandSubActionInOnCreateView[0] = true;
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new EnterTransitionFinish(first));
+ verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ nullable(Bundle.class), any(View.class));
+ assertTrue(first.getFragment().isExpanded());
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(first.getFragment().isExpanded());
+
+ sendKey(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
+ verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
+ }
+
+ @Test
+ public void setActionsWhenSubActionsExpanded() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+ subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+ actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+ .title("list").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) {
+ GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+ invocation.getMock();
+ GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+ if (action.getId() == 2000) {
+ List<GuidedAction> newActions = new ArrayList<GuidedAction>();
+ newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
+ obj.getFragment().setActions(newActions);
+ return false;
+ }
+ return false;
+ }
+ }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+ // after clicked, it sub actions list should expand
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(firstView.hasFocus());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+ ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+ verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+ // after clicked a sub action, whole action list is replaced.
+ PollingCheck.waitFor(new ExpandTransitionFinish(first));
+ assertFalse(first.getFragment().isExpanded());
+ View newFirstView = first.getFragment().getActionItemView(0);
+ assertTrue(newFirstView.hasFocus());
+ assertTrue(newFirstView.getVisibility() == View.VISIBLE);
+ GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
+ .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
+ assertEquals(1001, vh.getAction().getId());
+
+ }
+
+ @Test
+ public void buttonActionsRtl() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
+ return null;
+ }
+ }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ true, View.LAYOUT_DIRECTION_RTL);
+
+ assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ }
+
+ @Test
+ public void recyclerViewDiffTest() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ final GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action1").build());
+ actions.add(new GuidedAction.Builder().id(1001).title("action2").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+
+ launchTestActivity(firstFragmentName, true);
+
+ final ArrayList<RecyclerView.ViewHolder> changeList = new ArrayList();
+ VerticalGridView rv = first.getFragment().mActionsStylist.getActionsGridView();
+ rv.setItemAnimator(new DefaultItemAnimator() {
+ @Override
+ public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
+ if (!oldItem) {
+ changeList.add(item);
+ }
+ super.onChangeStarting(item, oldItem);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ List actions = new ArrayList();
+ actions.add(new GuidedAction.Builder().id(1001).title("action2x").build());
+ actions.add(new GuidedAction.Builder().id(1000).title("action1x").build());
+ first.getFragment().setActions(actions);
+ }
+ });
+
+ // should causes two change animation.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return changeList.size() == 2;
+ }
+ });
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
new file mode 100644
index 0000000..bac2f49
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepSupportFragmentTestActivity extends FragmentActivity {
+
+ /**
+ * Frst Test that will be included in this Activity
+ */
+ public static final String EXTRA_TEST_NAME = "testName";
+ /**
+ * True(default) to addAsRoot() for first Test, false to use add()
+ */
+ public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
+
+ /**
+ * Layout direction
+ */
+ public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+
+ int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
+ if (layoutDirection != -1) {
+ findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
+ }
+ if (savedInstanceState == null) {
+ String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
+ if (firstTestName != null) {
+ GuidedStepTestSupportFragment testFragment = new GuidedStepTestSupportFragment(firstTestName);
+ if (intent.getBooleanExtra(EXTRA_ADD_AS_ROOT, true)) {
+ GuidedStepTestSupportFragment.addAsRoot(this, testFragment, android.R.id.content);
+ } else {
+ GuidedStepTestSupportFragment.add(getSupportFragmentManager(), testFragment,
+ android.R.id.content);
+ }
+ }
+ }
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
new file mode 100644
index 0000000..12e4d09
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepSupportFragmentTestBase {
+
+ private static final long TIMEOUT = 5000;
+
+ @Rule public TestName mUnitTestName = new TestName();
+
+ @Rule
+ public ActivityTestRule<GuidedStepSupportFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(GuidedStepSupportFragmentTestActivity.class, false, false);
+
+ @Before
+ public void clearTests() {
+ GuidedStepTestSupportFragment.clearTests();
+ }
+
+ public static class ExpandTransitionFinish extends PollingCheck.PollingCheckCondition {
+ GuidedStepTestSupportFragment.Provider mProvider;
+
+ public ExpandTransitionFinish(GuidedStepTestSupportFragment.Provider provider) {
+ mProvider = provider;
+ }
+
+ @Override
+ public boolean canPreProceed() {
+ return false;
+ }
+
+ @Override
+ public boolean canProceed() {
+ GuidedStepTestSupportFragment fragment = mProvider.getFragment();
+ if (fragment != null && fragment.getView() != null) {
+ if (!fragment.getGuidedActionsStylist().isInExpandTransition()) {
+ // expand transition finishes
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public static void waitOnDestroy(GuidedStepTestSupportFragment.Provider provider,
+ int times) {
+ verify(provider, timeout((int)TIMEOUT).times(times)).onDestroy();
+ }
+
+ public static class EnterTransitionFinish extends PollingCheck.PollingCheckCondition {
+ PollingCheck.ViewScreenPositionDetector mDector =
+ new PollingCheck.ViewScreenPositionDetector();
+
+ GuidedStepTestSupportFragment.Provider mProvider;
+
+ public EnterTransitionFinish(GuidedStepTestSupportFragment.Provider provider) {
+ mProvider = provider;
+ }
+ @Override
+ public boolean canProceed() {
+ GuidedStepTestSupportFragment fragment = mProvider.getFragment();
+ if (fragment != null && fragment.getView() != null) {
+ View view = fragment.getView().findViewById(R.id.guidance_title);
+ if (view != null) {
+ if (mDector.isViewStableOnScreen(view)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ public static void sendKey(int keyCode) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
+ }
+
+ public String generateMethodTestName(String testName) {
+ return mUnitTestName.getMethodName() + "_" + testName;
+ }
+
+ public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot, int layoutDirection) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
+ return activityTestRule.launchActivity(intent);
+ }
+
+ public GuidedStepTestSupportFragment.Provider mockProvider(String testName) {
+ GuidedStepTestSupportFragment.Provider test = mock(GuidedStepTestSupportFragment.Provider.class);
+ when(test.getActivity()).thenCallRealMethod();
+ when(test.getFragmentManager()).thenCallRealMethod();
+ when(test.getFragment()).thenCallRealMethod();
+ GuidedStepTestSupportFragment.setupTest(testName, test);
+ return test;
+ }
+}
+
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
new file mode 100644
index 0000000..73e4083
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
@@ -0,0 +1,240 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepTestSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepTestFragment extends GuidedStepFragment {
+
+ private static final String KEY_TEST_NAME = "key_test_name";
+
+ private static final HashMap<String, Provider> sTestMap = new HashMap<String, Provider>();
+
+ public static class Provider {
+
+ GuidedStepTestFragment mFragment;
+
+ public void onCreate(Bundle savedInstanceState) {
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ }
+
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance("", "", "", null);
+ }
+
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ public void onGuidedActionClicked(GuidedAction action) {
+ }
+
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return true;
+ }
+
+ public void onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState, View result) {
+ }
+
+ public void onDestroyView() {
+ }
+
+ public void onDestroy() {
+ }
+
+ public void onStart() {
+ }
+
+ public void onStop() {
+ }
+
+ public void onResume() {
+ }
+
+ public void onPause() {
+ }
+
+ public void onViewStateRestored(Bundle bundle) {
+ }
+
+ public void onDetach() {
+ }
+
+ public GuidedStepTestFragment getFragment() {
+ return mFragment;
+ }
+
+ public Activity getActivity() {
+ return mFragment.getActivity();
+ }
+
+ public FragmentManager getFragmentManager() {
+ return mFragment.getFragmentManager();
+ }
+ }
+
+ public static void setupTest(String testName, Provider provider) {
+ sTestMap.put(testName, provider);
+ }
+
+ public static void clearTests() {
+ sTestMap.clear();
+ }
+
+ CharSequence mTestName;
+ Provider mProvider;
+
+ public GuidedStepTestFragment() {
+ }
+
+ public GuidedStepTestFragment(String testName) {
+ setTestName(testName);
+ }
+
+ public void setTestName(CharSequence testName) {
+ mTestName = testName;
+ }
+
+ public CharSequence getTestName() {
+ return mTestName;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mTestName = savedInstanceState.getCharSequence(KEY_TEST_NAME, null);
+ }
+ mProvider = sTestMap.get(mTestName);
+ if (mProvider == null) {
+ throw new IllegalArgumentException("you must setupTest()");
+ }
+ mProvider.mFragment = this;
+ super.onCreate(savedInstanceState);
+ mProvider.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(KEY_TEST_NAME, mTestName);
+ mProvider.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ Guidance g = mProvider.onCreateGuidance(savedInstanceState);
+ if (g == null) {
+ g = new Guidance("", "", "", null);
+ }
+ return g;
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mProvider.onCreateActions(actions, savedInstanceState);
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mProvider.onCreateButtonActions(actions, savedInstanceState);
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ mProvider.onGuidedActionClicked(action);
+ }
+
+ @Override
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return mProvider.onSubGuidedActionClicked(action);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
+ View view = super.onCreateView(inflater, container, state);
+ mProvider.onCreateView(inflater, container, state, view);
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mProvider.onDestroyView();
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mProvider.onDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onPause() {
+ mProvider.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mProvider.onResume();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mProvider.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ mProvider.onStop();
+ super.onStop();
+ }
+
+ @Override
+ public void onDetach() {
+ mProvider.onDetach();
+ super.onDetach();
+ }
+
+ @Override
+ public void onViewStateRestored(Bundle bundle) {
+ super.onViewStateRestored(bundle);
+ mProvider.onViewStateRestored(bundle);
+ }
+}
+
diff --git a/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
new file mode 100644
index 0000000..95491ce
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @hide from javadoc
+ */
+public class GuidedStepTestSupportFragment extends GuidedStepSupportFragment {
+
+ private static final String KEY_TEST_NAME = "key_test_name";
+
+ private static final HashMap<String, Provider> sTestMap = new HashMap<String, Provider>();
+
+ public static class Provider {
+
+ GuidedStepTestSupportFragment mFragment;
+
+ public void onCreate(Bundle savedInstanceState) {
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ }
+
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance("", "", "", null);
+ }
+
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ public void onGuidedActionClicked(GuidedAction action) {
+ }
+
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return true;
+ }
+
+ public void onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState, View result) {
+ }
+
+ public void onDestroyView() {
+ }
+
+ public void onDestroy() {
+ }
+
+ public void onStart() {
+ }
+
+ public void onStop() {
+ }
+
+ public void onResume() {
+ }
+
+ public void onPause() {
+ }
+
+ public void onViewStateRestored(Bundle bundle) {
+ }
+
+ public void onDetach() {
+ }
+
+ public GuidedStepTestSupportFragment getFragment() {
+ return mFragment;
+ }
+
+ public FragmentActivity getActivity() {
+ return mFragment.getActivity();
+ }
+
+ public FragmentManager getFragmentManager() {
+ return mFragment.getFragmentManager();
+ }
+ }
+
+ public static void setupTest(String testName, Provider provider) {
+ sTestMap.put(testName, provider);
+ }
+
+ public static void clearTests() {
+ sTestMap.clear();
+ }
+
+ CharSequence mTestName;
+ Provider mProvider;
+
+ public GuidedStepTestSupportFragment() {
+ }
+
+ public GuidedStepTestSupportFragment(String testName) {
+ setTestName(testName);
+ }
+
+ public void setTestName(CharSequence testName) {
+ mTestName = testName;
+ }
+
+ public CharSequence getTestName() {
+ return mTestName;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ mTestName = savedInstanceState.getCharSequence(KEY_TEST_NAME, null);
+ }
+ mProvider = sTestMap.get(mTestName);
+ if (mProvider == null) {
+ throw new IllegalArgumentException("you must setupTest()");
+ }
+ mProvider.mFragment = this;
+ super.onCreate(savedInstanceState);
+ mProvider.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(KEY_TEST_NAME, mTestName);
+ mProvider.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ Guidance g = mProvider.onCreateGuidance(savedInstanceState);
+ if (g == null) {
+ g = new Guidance("", "", "", null);
+ }
+ return g;
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mProvider.onCreateActions(actions, savedInstanceState);
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mProvider.onCreateButtonActions(actions, savedInstanceState);
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ mProvider.onGuidedActionClicked(action);
+ }
+
+ @Override
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ return mProvider.onSubGuidedActionClicked(action);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
+ View view = super.onCreateView(inflater, container, state);
+ mProvider.onCreateView(inflater, container, state, view);
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mProvider.onDestroyView();
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mProvider.onDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onPause() {
+ mProvider.onPause();
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mProvider.onResume();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mProvider.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ mProvider.onStop();
+ super.onStop();
+ }
+
+ @Override
+ public void onDetach() {
+ mProvider.onDetach();
+ super.onDetach();
+ }
+
+ @Override
+ public void onViewStateRestored(Bundle bundle) {
+ super.onViewStateRestored(bundle);
+ mProvider.onViewStateRestored(bundle);
+ }
+}
+
diff --git a/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
new file mode 100644
index 0000000..f23e38a
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
@@ -0,0 +1,129 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from HeadersSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.FocusHighlightHelper;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HeadersFragmentTest extends SingleFragmentTestBase {
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public static class F_defaultScale extends HeadersFragment {
+ final ListRowPresenter mPresenter = new ListRowPresenter();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+ }
+
+ @Test
+ public void defaultScale() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultScale.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
+ assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
+ }
+
+ public static class F_disableScale extends HeadersFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
+ }
+ }
+
+ @Test
+ public void disableScale() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(F_disableScale.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+ assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+ }
+
+ public static class F_disableScaleInConstructor extends HeadersFragment {
+ public F_disableScaleInConstructor() {
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter(), false);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+ }
+
+ @Test
+ public void disableScaleInConstructor() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ F_disableScaleInConstructor.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+ assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
new file mode 100644
index 0000000..436a797
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.FocusHighlightHelper;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HeadersSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public static class F_defaultScale extends HeadersSupportFragment {
+ final ListRowPresenter mPresenter = new ListRowPresenter();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+ }
+
+ @Test
+ public void defaultScale() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultScale.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
+ assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
+ }
+
+ public static class F_disableScale extends HeadersSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
+ }
+ }
+
+ @Test
+ public void disableScale() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_disableScale.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+ assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+ }
+
+ public static class F_disableScaleInConstructor extends HeadersSupportFragment {
+ public F_disableScaleInConstructor() {
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter(), false);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
+ setAdapter(adapter);
+ loadData(adapter, 10);
+ }
+ }
+
+ @Test
+ public void disableScaleInConstructor() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ F_disableScaleInConstructor.class, 1000);
+
+ final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(0);
+ assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
+ assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java b/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
rename to leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
diff --git a/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
new file mode 100644
index 0000000..a9101a7
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -0,0 +1,374 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.Suppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackFragmentTest extends SingleFragmentTestBase {
+
+ private static final String TAG = "PlaybackFragmentTest";
+ private static final long TRANSITION_LENGTH = 1000;
+
+ @Test
+ public void testDetachCalledWhenDestroyFragment() throws Throwable {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ final PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
+ PlaybackGlue glue = fragment.getGlue();
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.finish();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mDestroyCalled;
+ }
+ });
+ assertNull(glue.getHost());
+ }
+
+ @Test
+ public void testSelectedListener() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewSelectedListener selectedListener = Mockito.mock(
+ OnItemViewSelectedListener.class);
+ fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(fragment.getVerticalGridView());
+ verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+ // selected.
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+ listRowItemPassed);
+ }
+
+ @Test
+ public void testClickedListener() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+ fragment.setOnItemViewClickedListener(clickedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(fragment.getVerticalGridView());
+ verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the click listener.",
+ listRowItemPassed);
+ }
+
+ @FlakyTest
+ @Suppress
+ @Test
+ public void alignmentRowToBottom() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+ final PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() > 2);
+
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ fragment.getVerticalGridView().setSelectedPositionSmooth(
+ fragment.getAdapter().size() - 1);
+ }
+ });
+ waitForScrollIdle(fragment.getVerticalGridView());
+
+ View lastRow = fragment.getVerticalGridView().getChildAt(
+ fragment.getVerticalGridView().getChildCount() - 1);
+ assertEquals(fragment.getAdapter().size() - 1,
+ fragment.getVerticalGridView().getChildAdapterPosition(lastRow));
+ assertTrue(lastRow.hasFocus());
+ assertEquals(lastRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - lastRow.getBottom());
+ }
+
+ public static class PurePlaybackFragment extends PlaybackFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setFadingEnabled(false);
+ PlaybackControlsRow row = new PlaybackControlsRow();
+ SparseArrayObjectAdapter primaryAdapter = new SparseArrayObjectAdapter(
+ new ControlButtonPresenterSelector());
+ primaryAdapter.set(0, new PlaybackControlsRow.SkipPreviousAction(getActivity()));
+ primaryAdapter.set(1, new PlaybackControlsRow.PlayPauseAction(getActivity()));
+ primaryAdapter.set(2, new PlaybackControlsRow.SkipNextAction(getActivity()));
+ row.setPrimaryActionsAdapter(primaryAdapter);
+ row.setSecondaryActionsAdapter(null);
+ setPlaybackRow(row);
+ setPlaybackRowPresenter(new PlaybackControlsRowPresenter());
+ }
+ }
+
+ @Test
+ public void setupRowAndPresenterWithoutGlue() {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(PurePlaybackFragment.class, 1000);
+ final PurePlaybackFragment fragment = (PurePlaybackFragment)
+ activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() == 1);
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+ }
+
+ public static class ControlGlueFragment extends PlaybackFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int[] ffspeeds = new int[] {PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0,
+ PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1};
+ PlaybackGlue glue = new PlaybackControlGlue(
+ getActivity(), ffspeeds) {
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return false;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return "Title";
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return "SubTitle";
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return 100;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return PlaybackControlGlue.ACTION_PLAY_PAUSE;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return 50;
+ }
+ };
+ glue.setHost(new PlaybackFragmentGlueHost(this));
+ }
+ }
+
+ @Test
+ public void setupWithControlGlue() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(ControlGlueFragment.class, 1000);
+ final ControlGlueFragment fragment = (ControlGlueFragment)
+ activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() == 1);
+
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
new file mode 100644
index 0000000..4aaeae8
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.Suppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ private static final String TAG = "PlaybackSupportFragmentTest";
+ private static final long TRANSITION_LENGTH = 1000;
+
+ @Test
+ public void testDetachCalledWhenDestroyFragment() throws Throwable {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
+ PlaybackGlue glue = fragment.getGlue();
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.finish();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mDestroyCalled;
+ }
+ });
+ assertNull(glue.getHost());
+ }
+
+ @Test
+ public void testSelectedListener() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewSelectedListener selectedListener = Mockito.mock(
+ OnItemViewSelectedListener.class);
+ fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(fragment.getVerticalGridView());
+ verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+ // selected.
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+ listRowItemPassed);
+ }
+
+ @Test
+ public void testClickedListener() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+ fragment.setOnItemViewClickedListener(clickedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(fragment.getVerticalGridView());
+ verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the click listener.",
+ listRowItemPassed);
+ }
+
+ @FlakyTest
+ @Suppress
+ @Test
+ public void alignmentRowToBottom() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+ final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() > 2);
+
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+
+ activityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ fragment.getVerticalGridView().setSelectedPositionSmooth(
+ fragment.getAdapter().size() - 1);
+ }
+ });
+ waitForScrollIdle(fragment.getVerticalGridView());
+
+ View lastRow = fragment.getVerticalGridView().getChildAt(
+ fragment.getVerticalGridView().getChildCount() - 1);
+ assertEquals(fragment.getAdapter().size() - 1,
+ fragment.getVerticalGridView().getChildAdapterPosition(lastRow));
+ assertTrue(lastRow.hasFocus());
+ assertEquals(lastRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - lastRow.getBottom());
+ }
+
+ public static class PurePlaybackSupportFragment extends PlaybackSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setFadingEnabled(false);
+ PlaybackControlsRow row = new PlaybackControlsRow();
+ SparseArrayObjectAdapter primaryAdapter = new SparseArrayObjectAdapter(
+ new ControlButtonPresenterSelector());
+ primaryAdapter.set(0, new PlaybackControlsRow.SkipPreviousAction(getActivity()));
+ primaryAdapter.set(1, new PlaybackControlsRow.PlayPauseAction(getActivity()));
+ primaryAdapter.set(2, new PlaybackControlsRow.SkipNextAction(getActivity()));
+ row.setPrimaryActionsAdapter(primaryAdapter);
+ row.setSecondaryActionsAdapter(null);
+ setPlaybackRow(row);
+ setPlaybackRowPresenter(new PlaybackControlsRowPresenter());
+ }
+ }
+
+ @Test
+ public void setupRowAndPresenterWithoutGlue() {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(PurePlaybackSupportFragment.class, 1000);
+ final PurePlaybackSupportFragment fragment = (PurePlaybackSupportFragment)
+ activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() == 1);
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+ }
+
+ public static class ControlGlueFragment extends PlaybackSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int[] ffspeeds = new int[] {PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0,
+ PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1};
+ PlaybackGlue glue = new PlaybackControlGlue(
+ getActivity(), ffspeeds) {
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return false;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return "Title";
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return "SubTitle";
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return 100;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return PlaybackControlGlue.ACTION_PLAY_PAUSE;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return 50;
+ }
+ };
+ glue.setHost(new PlaybackSupportFragmentGlueHost(this));
+ }
+ }
+
+ @Test
+ public void setupWithControlGlue() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(ControlGlueFragment.class, 1000);
+ final ControlGlueFragment fragment = (ControlGlueFragment)
+ activity.getTestFragment();
+
+ assertTrue(fragment.getAdapter().size() == 1);
+
+ View playRow = fragment.getVerticalGridView().getChildAt(0);
+ assertTrue(playRow.hasFocus());
+ assertEquals(playRow.getResources().getDimensionPixelSize(
+ android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
+ fragment.getVerticalGridView().getHeight() - playRow.getBottom());
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
new file mode 100644
index 0000000..47b644c
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -0,0 +1,371 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackTestSupportFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackTestFragment extends PlaybackFragment {
+ private static final String TAG = "PlaybackTestFragment";
+
+ /**
+ * Change this to choose a different overlay background.
+ */
+ private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+
+ /**
+ * Change this to select hidden
+ */
+ private static final boolean SECONDARY_HIDDEN = false;
+
+ /**
+ * Change the number of related content rows.
+ */
+ private static final int RELATED_CONTENT_ROWS = 3;
+
+ private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+ boolean mDestroyCalled;
+
+ @Override
+ public SparseArrayObjectAdapter getAdapter() {
+ return (SparseArrayObjectAdapter) super.getAdapter();
+ }
+
+ private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ };
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mDestroyCalled = true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ setBackgroundType(BACKGROUND_TYPE);
+
+ createComponents(getActivity());
+ setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+
+ private void createComponents(Context context) {
+ mGlue = new PlaybackControlHelper(context) {
+ @Override
+ public int getUpdatePeriod() {
+ long totalTime = getControlsRow().getDuration();
+ if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+ return 1000;
+ }
+ return 16;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == R.id.lb_control_picture_in_picture) {
+ getActivity().enterPictureInPictureMode();
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ protected void onCreateControlsRowAndPresenter() {
+ super.onCreateControlsRowAndPresenter();
+ getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+ }
+ };
+
+ mGlue.setHost(new PlaybackFragmentGlueHost(this));
+ ClassPresenterSelector selector = new ClassPresenterSelector();
+ selector.addClassPresenter(ListRow.class, new ListRowPresenter());
+
+ setAdapter(new SparseArrayObjectAdapter(selector));
+
+ // Add related content rows
+ for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+ listRowAdapter.add("Some related content");
+ listRowAdapter.add("Other related content");
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ getAdapter().set(1 + i, new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public PlaybackControlGlue getGlue() {
+ return mGlue;
+ }
+
+ abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+ /**
+ * Change the location of the thumbs up/down controls
+ */
+ private static final boolean THUMBS_PRIMARY = true;
+
+ private static final String FAUX_TITLE = "A short song of silence";
+ private static final String FAUX_SUBTITLE = "2014";
+ private static final int FAUX_DURATION = 33 * 1000;
+
+ // These should match the playback service FF behavior
+ private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+ private boolean mIsPlaying;
+ private int mSpeed = PLAYBACK_SPEED_PAUSED;
+ private long mStartTime;
+ private long mStartPosition = 0;
+
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+ private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+ private PlaybackControlsRow.PictureInPictureAction mPipAction;
+ private static Handler sProgressHandler = new Handler();
+
+ private final Runnable mUpdateProgressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateProgress();
+ sProgressHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+
+ PlaybackControlHelper(Context context) {
+ super(context, sFastForwardSpeeds);
+ mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+ mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
+ mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+ mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+ mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+ PresenterSelector presenterSelector) {
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+ if (THUMBS_PRIMARY) {
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+ }
+ return adapter;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return true;
+ }
+ }
+ return super.onKey(view, keyCode, keyEvent);
+ }
+
+ private boolean shouldDispatchAction(Action action) {
+ return action == mRepeatAction || action == mThumbsUpAction
+ || action == mThumbsDownAction;
+ }
+
+ private void dispatchAction(Action action) {
+ Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+ PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+ multiAction.nextIndex();
+ notifyActionChanged(multiAction);
+ }
+
+ private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+ int index;
+ index = getPrimaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ } else {
+ index = getSecondaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ }
+
+ private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+ return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+ }
+
+ private ArrayObjectAdapter getSecondaryActionsAdapter() {
+ return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return mIsPlaying;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return FAUX_TITLE;
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return FAUX_SUBTITLE;
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return FAUX_DURATION;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return mSpeed;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ int speed;
+ if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+ speed = 0;
+ } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+ speed = 1;
+ } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = getFastForwardSpeeds()[index];
+ } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = -getRewindSpeeds()[index];
+ } else {
+ return -1;
+ }
+ long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+ if (position > getMediaDuration()) {
+ position = getMediaDuration();
+ onPlaybackComplete(true);
+ } else if (position < 0) {
+ position = 0;
+ onPlaybackComplete(false);
+ }
+ return (int) position;
+ }
+
+ void onPlaybackComplete(final boolean ended) {
+ sProgressHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) {
+ pause();
+ } else {
+ play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+ }
+ mStartPosition = 0;
+ onStateChanged();
+ }
+ });
+ }
+
+ @Override
+ public void play(int speed) {
+ if (speed == mSpeed) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = speed;
+ mIsPlaying = true;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void pause() {
+ if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = PLAYBACK_SPEED_PAUSED;
+ mIsPlaying = false;
+ }
+
+ @Override
+ public void next() {
+ // Not supported
+ }
+
+ @Override
+ public void previous() {
+ // Not supported
+ }
+
+ @Override
+ public void enableProgressUpdating(boolean enable) {
+ sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+ if (enable) {
+ mUpdateProgressRunnable.run();
+ }
+ }
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
new file mode 100644
index 0000000..dc93a1c
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackTestSupportFragment extends PlaybackSupportFragment {
+ private static final String TAG = "PlaybackTestSupportFragment";
+
+ /**
+ * Change this to choose a different overlay background.
+ */
+ private static final int BACKGROUND_TYPE = PlaybackSupportFragment.BG_LIGHT;
+
+ /**
+ * Change this to select hidden
+ */
+ private static final boolean SECONDARY_HIDDEN = false;
+
+ /**
+ * Change the number of related content rows.
+ */
+ private static final int RELATED_CONTENT_ROWS = 3;
+
+ private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+ boolean mDestroyCalled;
+
+ @Override
+ public SparseArrayObjectAdapter getAdapter() {
+ return (SparseArrayObjectAdapter) super.getAdapter();
+ }
+
+ private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ };
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mDestroyCalled = true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ setBackgroundType(BACKGROUND_TYPE);
+
+ createComponents(getActivity());
+ setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+
+ private void createComponents(Context context) {
+ mGlue = new PlaybackControlHelper(context) {
+ @Override
+ public int getUpdatePeriod() {
+ long totalTime = getControlsRow().getDuration();
+ if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+ return 1000;
+ }
+ return 16;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == R.id.lb_control_picture_in_picture) {
+ getActivity().enterPictureInPictureMode();
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ protected void onCreateControlsRowAndPresenter() {
+ super.onCreateControlsRowAndPresenter();
+ getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+ }
+ };
+
+ mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
+ ClassPresenterSelector selector = new ClassPresenterSelector();
+ selector.addClassPresenter(ListRow.class, new ListRowPresenter());
+
+ setAdapter(new SparseArrayObjectAdapter(selector));
+
+ // Add related content rows
+ for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+ listRowAdapter.add("Some related content");
+ listRowAdapter.add("Other related content");
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ getAdapter().set(1 + i, new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public PlaybackControlGlue getGlue() {
+ return mGlue;
+ }
+
+ abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+ /**
+ * Change the location of the thumbs up/down controls
+ */
+ private static final boolean THUMBS_PRIMARY = true;
+
+ private static final String FAUX_TITLE = "A short song of silence";
+ private static final String FAUX_SUBTITLE = "2014";
+ private static final int FAUX_DURATION = 33 * 1000;
+
+ // These should match the playback service FF behavior
+ private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+ private boolean mIsPlaying;
+ private int mSpeed = PLAYBACK_SPEED_PAUSED;
+ private long mStartTime;
+ private long mStartPosition = 0;
+
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+ private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+ private PlaybackControlsRow.PictureInPictureAction mPipAction;
+ private static Handler sProgressHandler = new Handler();
+
+ private final Runnable mUpdateProgressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateProgress();
+ sProgressHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+
+ PlaybackControlHelper(Context context) {
+ super(context, sFastForwardSpeeds);
+ mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+ mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
+ mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+ mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+ mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+ PresenterSelector presenterSelector) {
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+ if (THUMBS_PRIMARY) {
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+ }
+ return adapter;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return true;
+ }
+ }
+ return super.onKey(view, keyCode, keyEvent);
+ }
+
+ private boolean shouldDispatchAction(Action action) {
+ return action == mRepeatAction || action == mThumbsUpAction
+ || action == mThumbsDownAction;
+ }
+
+ private void dispatchAction(Action action) {
+ Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+ PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+ multiAction.nextIndex();
+ notifyActionChanged(multiAction);
+ }
+
+ private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+ int index;
+ index = getPrimaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ } else {
+ index = getSecondaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ }
+
+ private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+ return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+ }
+
+ private ArrayObjectAdapter getSecondaryActionsAdapter() {
+ return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return mIsPlaying;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return FAUX_TITLE;
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return FAUX_SUBTITLE;
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return FAUX_DURATION;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return mSpeed;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ int speed;
+ if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+ speed = 0;
+ } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+ speed = 1;
+ } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = getFastForwardSpeeds()[index];
+ } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = -getRewindSpeeds()[index];
+ } else {
+ return -1;
+ }
+ long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+ if (position > getMediaDuration()) {
+ position = getMediaDuration();
+ onPlaybackComplete(true);
+ } else if (position < 0) {
+ position = 0;
+ onPlaybackComplete(false);
+ }
+ return (int) position;
+ }
+
+ void onPlaybackComplete(final boolean ended) {
+ sProgressHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) {
+ pause();
+ } else {
+ play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+ }
+ mStartPosition = 0;
+ onStateChanged();
+ }
+ });
+ }
+
+ @Override
+ public void play(int speed) {
+ if (speed == mSpeed) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = speed;
+ mIsPlaying = true;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void pause() {
+ if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = PLAYBACK_SPEED_PAUSED;
+ mIsPlaying = false;
+ }
+
+ @Override
+ public void next() {
+ // Not supported
+ }
+
+ @Override
+ public void previous() {
+ // Not supported
+ }
+
+ @Override
+ public void enableProgressUpdating(boolean enable) {
+ sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+ if (enable) {
+ mUpdateProgressRunnable.run();
+ }
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java b/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
diff --git a/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
new file mode 100644
index 0000000..dc10a05
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -0,0 +1,1354 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RowsFragmentTest extends SingleFragmentTestBase {
+
+ static final StringPresenter sCardPresenter = new StringPresenter();
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < repeatPerRow; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ static Bundle saveActivityState(final SingleFragmentTestActivity activity) {
+ final Bundle[] savedState = new Bundle[1];
+ // save activity state
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ savedState[0] = activity.performSaveInstanceState();
+ }
+ });
+ return savedState[0];
+ }
+
+ static void waitForHeaderTransition(final F_Base fragment) {
+ // Wait header transition finishes
+ SystemClock.sleep(100);
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !fragment.isInHeadersTransition();
+ }
+ });
+ }
+
+ static void selectAndWaitFragmentAnimation(final F_Base fragment, final int row,
+ final int item) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(row, true,
+ new ListRowPresenter.SelectItemViewHolderTask(item));
+ }
+ });
+ // Wait header transition finishes and scrolling stops
+ SystemClock.sleep(100);
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !fragment.isInHeadersTransition()
+ && !fragment.getHeadersFragment().isScrolling();
+ }
+ });
+ }
+
+ public static class F_defaultAlignment extends RowsFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void defaultAlignment() throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
+
+ final Rect rect = new Rect();
+
+ final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+ rect.set(0, 0, row0.getWidth(), row0.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row0, rect);
+ assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(gridView);
+ View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+ rect.set(0, 0, row1.getWidth(), row1.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row1, rect);
+ assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+ }
+
+ public static class F_selectBeforeSetAdapter extends RowsFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setSelectedPosition(7, false);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectBeforeSetAdapter() throws InterruptedException {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+ final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ public static class F_selectBeforeAddData extends RowsFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ setSelectedPosition(7, false);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ loadData(adapter, 10, 1);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectBeforeAddData() throws InterruptedException {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+ final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ public static class F_selectAfterAddData extends RowsFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setSelectedPosition(7, false);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectAfterAddData() throws InterruptedException {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+ final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+ public static class F_restoreSelection extends RowsFragment {
+ public F_restoreSelection() {
+ sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ if (savedInstanceState == null) {
+ setSelectedPosition(7, false);
+ }
+ }
+ }
+
+ @Test
+ public void restoreSelection() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ }
+ );
+ SystemClock.sleep(1000);
+
+ // mActivity is invalid after recreate(), a new Activity instance is created
+ // but we could get Fragment from static variable.
+ RowsFragment fragment = sLastF_restoreSelection.get();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+ }
+
+ public static class F_ListRowWithOnClick extends RowsFragment {
+ Presenter.ViewHolder mLastClickedItemViewHolder;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setOnItemViewClickedListener(new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ mLastClickedItemViewHolder = itemViewHolder;
+ }
+ });
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void prefetchChildItemsBeforeAttach() throws Throwable {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+ F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+ final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ gridView.setSelectedPositionSmooth(lastRowPos);
+ }
+ }
+ );
+ waitForScrollIdle(gridView);
+ ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+ RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+ final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+ prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+ fragment.mLastClickedItemViewHolder = null;
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+ }
+ }
+ );
+ assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+ }
+
+ @Test
+ public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
+ SingleFragmentTestActivity activity =
+ launchAndWaitActivity(RowsFragment.class, 2000);
+ final RowsFragment fragment = (RowsFragment) activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ ObjectAdapter adapter = new ObjectAdapter() {
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ @Override
+ public Object get(int position) {
+ return null;
+ }
+
+ @Override
+ public long getId(int position) {
+ return 1;
+ }
+ };
+ adapter.setHasStableIds(true);
+ fragment.setAdapter(adapter);
+ }
+ }
+ );
+ }
+
+ static class StableIdAdapter extends ObjectAdapter {
+ ArrayList<Integer> mList = new ArrayList();
+
+ @Override
+ public long getId(int position) {
+ return mList.get(position).longValue();
+ }
+
+ @Override
+ public Object get(int position) {
+ return mList.get(position);
+ }
+
+ @Override
+ public int size() {
+ return mList.size();
+ }
+ }
+
+ public static class F_rowNotifyItemRangeChange extends BrowseFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 2; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(true);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ setAdapter(adapter);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ StableIdAdapter rowAdapter = (StableIdAdapter)
+ ((ListRow) adapter.get(1)).getAdapter();
+ rowAdapter.notifyItemRangeChanged(0, 3);
+ }
+ }, 500);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void rowNotifyItemRangeChange() throws InterruptedException {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
+
+ VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
+ .getRowsFragment().getVerticalGridView();
+ for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+ HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+ .findViewById(R.id.row_content);
+ for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+ assertEquals(horizontalGridView.getPaddingTop(),
+ horizontalGridView.getChildAt(j).getTop());
+ }
+ }
+ }
+
+ public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ prepareEntranceTransition();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 2; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(true);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ setAdapter(adapter);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ StableIdAdapter rowAdapter = (StableIdAdapter)
+ ((ListRow) adapter.get(1)).getAdapter();
+ rowAdapter.notifyItemRangeChanged(0, 3);
+ }
+ }, 500);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ startEntranceTransition();
+ }
+ }, 520);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
+
+ VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
+ .getRowsFragment().getVerticalGridView();
+ for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+ HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+ .findViewById(R.id.row_content);
+ for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+ assertEquals(horizontalGridView.getPaddingTop(),
+ horizontalGridView.getChildAt(j).getTop());
+ assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
+ }
+ }
+ }
+
+ public static class F_Base extends BrowseFragment {
+
+ List<Long> mEntranceTransitionStartTS = new ArrayList();
+ List<Long> mEntranceTransitionEndTS = new ArrayList();
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ super.onEntranceTransitionStart();
+ mEntranceTransitionStartTS.add(SystemClock.uptimeMillis());
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ super.onEntranceTransitionEnd();
+ mEntranceTransitionEndTS.add(SystemClock.uptimeMillis());
+ }
+
+ public void assertExecutedEntranceTransition() {
+ assertEquals(1, mEntranceTransitionStartTS.size());
+ assertEquals(1, mEntranceTransitionEndTS.size());
+ assertTrue(mEntranceTransitionEndTS.get(0) - mEntranceTransitionStartTS.get(0) > 100);
+ }
+
+ public void assertNoEntranceTransition() {
+ assertEquals(0, mEntranceTransitionStartTS.size());
+ assertEquals(0, mEntranceTransitionEndTS.size());
+ }
+
+ /**
+ * Util to wait PageFragment swapped.
+ */
+ Fragment waitPageFragment(final Class pageFragmentClass) {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return pageFragmentClass.isInstance(getMainFragment())
+ && getMainFragment().getView() != null;
+ }
+ });
+ return getMainFragment();
+ }
+
+ /**
+ * Wait until a fragment for non-page Row is created. Does not apply to the case a
+ * RowsFragment is created on a PageRow.
+ */
+ RowsFragment waitRowsFragment() {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mMainFragmentListRowDataAdapter != null
+ && getMainFragment() instanceof RowsFragment
+ && !(getMainFragment() instanceof SampleRowsFragment);
+ }
+ });
+ return (RowsFragment) getMainFragment();
+ }
+ }
+
+ static ObjectAdapter createListRowAdapter() {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ return listRowAdapter;
+ }
+
+ /**
+ * Create BrowseFragmentAdapter with 3 ListRows
+ */
+ static ArrayObjectAdapter createListRowsAdapter() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 3; i++) {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ return adapter;
+ }
+
+ /**
+ * A typical BrowseFragment with multiple rows that start entrance transition
+ */
+ public static class F_standard extends F_Base {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(createListRowsAdapter());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentSetNullAdapter() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(null);
+ }
+ });
+ // adapter should no longer has observer and there is no reference to adapter from
+ // BrowseFragment.
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertNull(fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ // RowsFragment is still there
+ assertTrue(fragment.mMainFragment instanceof RowsFragment);
+ assertNotNull(fragment.mMainFragmentRowsAdapter);
+ assertNotNull(fragment.mMainFragmentAdapter);
+
+ // initialize to same adapter
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter1);
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapter() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ final ObjectAdapter adapter2 = createListRowsAdapter();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ final ObjectAdapter adapter2 = create2PageRow3ListRow();
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitPageFragment(SampleRowsFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangeListRowToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new MyPageRow(0));
+ }
+ });
+ fragment.waitPageFragment(SampleRowsFragment.class);
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangeListRowToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.replace(0, new MyPageRow(0));
+ }
+ });
+ fragment.waitPageFragment(SampleRowsFragment.class);
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangeListRowToListRow() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangeListRowToListRow() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.replace(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterPageToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ final ObjectAdapter adapter2 = create2PageRow3ListRow();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitPageFragment(SampleRowsFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyChangePageToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new MyPageRow(1));
+ }
+ });
+ fragment.waitPageFragment(SampleFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangePageToPage() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.replace(0, new MyPageRow(1));
+ }
+ });
+ fragment.waitPageFragment(SampleFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterPageToListRow() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ final ObjectAdapter adapter2 = createListRowsAdapter();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitRowsFragment();
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertTrue(adapter2.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangePageToListRow() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ fragment.waitRowsFragment();
+ assertTrue(adapter1.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangePageToListRow() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.replace(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ fragment.waitRowsFragment();
+ assertTrue(adapter1.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentRestore() throws InterruptedException {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select item 2 on row 1
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
+ // save activity to state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ // recreate activity with saved state
+ SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
+ // validate restored activity selected row and selected item
+ fragment2.assertNoEntranceTransition();
+ assertEquals(1, fragment2.getSelectedPosition());
+ assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+ .getSelectedPosition());
+ activity2.finish();
+ }
+
+ public static class MyPageRow extends PageRow {
+ public int type;
+ public MyPageRow(int type) {
+ super(new HeaderItem(100 + type, "page type " + type));
+ this.type = type;
+ }
+ }
+
+ /**
+ * A RowsFragment that is a separate page in BrowseFragment.
+ */
+ public static class SampleRowsFragment extends RowsFragment {
+ public SampleRowsFragment() {
+ // simulates late data loading:
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(createListRowsAdapter());
+ if (getMainFragmentAdapter() != null) {
+ getMainFragmentAdapter().getFragmentHost()
+ .notifyDataReady(getMainFragmentAdapter());
+ }
+ }
+ }, 500);
+ }
+ }
+
+ /**
+ * A custom Fragment that is a separate page in BrowseFragment.
+ */
+ public static class SampleFragment extends Fragment implements
+ BrowseFragment.MainFragmentAdapterProvider {
+
+ public static class PageFragmentAdapterImpl extends
+ BrowseFragment.MainFragmentAdapter<SampleFragment> {
+
+ public PageFragmentAdapterImpl(SampleFragment fragment) {
+ super(fragment);
+ setScalingEnabled(true);
+ }
+
+ @Override
+ public void setEntranceTransitionState(boolean state) {
+ getFragment().setEntranceTransitionState(state);
+ }
+ }
+
+ final PageFragmentAdapterImpl mMainFragmentAdapter = new PageFragmentAdapterImpl(this);
+
+ void setEntranceTransitionState(boolean state) {
+ final View view = getView();
+ int visibility = state ? View.VISIBLE : View.INVISIBLE;
+ view.findViewById(R.id.tv1).setVisibility(visibility);
+ view.findViewById(R.id.tv2).setVisibility(visibility);
+ view.findViewById(R.id.tv3).setVisibility(visibility);
+ }
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.page_fragment, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ // static layout has view and data ready immediately
+ mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
+ mMainFragmentAdapter.getFragmentHost().notifyDataReady(mMainFragmentAdapter);
+ }
+
+ @Override
+ public BrowseFragment.MainFragmentAdapter getMainFragmentAdapter() {
+ return mMainFragmentAdapter;
+ }
+ }
+
+ /**
+ * Create BrowseFragmentAdapter with 3 ListRows and 2 PageRows
+ */
+ private static ArrayObjectAdapter create3ListRow2PageRowAdapter() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 3; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ adapter.add(new MyPageRow(0));
+ adapter.add(new MyPageRow(1));
+ return adapter;
+ }
+
+ /**
+ * Create BrowseFragmentAdapter with 2 PageRows then 3 ListRow
+ */
+ private static ArrayObjectAdapter create2PageRow3ListRow() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ adapter.add(new MyPageRow(0));
+ adapter.add(new MyPageRow(1));
+ for (int i = 0; i < 3; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ return adapter;
+ }
+
+ static class MyFragmentFactory extends BrowseFragment.FragmentFactory {
+ @Override
+ public Fragment createFragment(Object rowObj) {
+ MyPageRow row = (MyPageRow) rowObj;
+ if (row.type == 0) {
+ return new SampleRowsFragment();
+ } else if (row.type == 1) {
+ return new SampleFragment();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A BrowseFragment with three ListRows, one SampleRowsFragment and one SampleFragment.
+ */
+ public static class F_3ListRow2PageRow extends F_Base {
+ public F_3ListRow2PageRow() {
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(create3ListRow2PageRowAdapter());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ /**
+ * A BrowseFragment with three ListRows, one SampleRowsFragment and one SampleFragment.
+ */
+ public static class F_2PageRow3ListRow extends F_Base {
+ public F_2PageRow3ListRow() {
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(create2PageRow3ListRow());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseFragmentRestoreToListRow() throws Throwable {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select item 2 on row 1.
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ // start a new activity with the state
+ SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsFragmentTest.F_standard.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ assertFalse(fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ assertEquals(1, fragment2.getSelectedPosition());
+ assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+ .getSelectedPosition());
+ activity2.finish();
+ }
+
+ void mixedBrowseFragmentRestoreToSampleRowsFragment(final boolean hideFastLane)
+ throws Throwable {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select row 3 which is mapped to SampleRowsFragment.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(3, true);
+ }
+ });
+ // Wait SampleRowsFragment being created
+ final SampleRowsFragment mainFragment = (SampleRowsFragment) fragment.waitPageFragment(
+ SampleRowsFragment.class);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ if (hideFastLane) {
+ fragment.startHeadersTransition(false);
+ }
+ }
+ });
+ // Wait header transition finishes
+ waitForHeaderTransition(fragment);
+ // Select item 1 on row 1 in SampleRowsFragment
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mainFragment.setSelectedPosition(1, true,
+ new ListRowPresenter.SelectItemViewHolderTask(1));
+ }
+ });
+ // Save activity state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsFragmentTest.F_3ListRow2PageRow.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ final SampleRowsFragment mainFragment2 = (SampleRowsFragment) fragment2.waitPageFragment(
+ SampleRowsFragment.class);
+ assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ // Validate BrowseFragment selected row 3 (mapped to SampleRowsFragment)
+ assertEquals(3, fragment2.getSelectedPosition());
+ // Validate SampleRowsFragment's selected row and selected item
+ assertEquals(1, mainFragment2.getSelectedPosition());
+ assertEquals(1, ((ListRowPresenter.ViewHolder) mainFragment2
+ .findRowViewHolderByPosition(1)).getSelectedPosition());
+ activity2.finish();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseFragmentRestoreToSampleRowsFragmentHideFastLane() throws Throwable {
+ mixedBrowseFragmentRestoreToSampleRowsFragment(true);
+
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseFragmentRestoreToSampleRowsFragmentShowFastLane() throws Throwable {
+ mixedBrowseFragmentRestoreToSampleRowsFragment(false);
+ }
+
+ void mixedBrowseFragmentRestoreToSampleFragment(final boolean hideFastLane)
+ throws Throwable {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(
+ RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select row 3 which is mapped to SampleFragment.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(4, true);
+ }
+ });
+ // Wait SampleFragment to be created
+ final SampleFragment mainFragment = (SampleFragment) fragment.waitPageFragment(
+ SampleFragment.class);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ if (hideFastLane) {
+ fragment.startHeadersTransition(false);
+ }
+ }
+ });
+ waitForHeaderTransition(fragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ // change TextView content which should be saved in states.
+ TextView t = mainFragment.getView().findViewById(R.id.tv2);
+ t.setText("changed text");
+ }
+ });
+ // Save activity state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsFragmentTest.F_3ListRow2PageRow.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
+ SampleFragment.class);
+ assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ // Validate BrowseFragment selected row 3 (mapped to SampleFragment)
+ assertEquals(4, fragment2.getSelectedPosition());
+ // Validate SampleFragment's view states are restored
+ TextView t = mainFragment2.getView().findViewById(R.id.tv2);
+ assertEquals("changed text", t.getText().toString());
+ activity2.finish();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseFragmentRestoreToSampleFragmentHideFastLane() throws Throwable {
+ mixedBrowseFragmentRestoreToSampleFragment(true);
+
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseFragmentRestoreToSampleFragmentShowFastLane() throws Throwable {
+ mixedBrowseFragmentRestoreToSampleFragment(false);
+ }
+
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
new file mode 100644
index 0000000..eca3f09
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -0,0 +1,1351 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RowsSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ static final StringPresenter sCardPresenter = new StringPresenter();
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < repeatPerRow; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ static Bundle saveActivityState(final SingleSupportFragmentTestActivity activity) {
+ final Bundle[] savedState = new Bundle[1];
+ // save activity state
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ savedState[0] = activity.performSaveInstanceState();
+ }
+ });
+ return savedState[0];
+ }
+
+ static void waitForHeaderTransition(final F_Base fragment) {
+ // Wait header transition finishes
+ SystemClock.sleep(100);
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !fragment.isInHeadersTransition();
+ }
+ });
+ }
+
+ static void selectAndWaitFragmentAnimation(final F_Base fragment, final int row,
+ final int item) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(row, true,
+ new ListRowPresenter.SelectItemViewHolderTask(item));
+ }
+ });
+ // Wait header transition finishes and scrolling stops
+ SystemClock.sleep(100);
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !fragment.isInHeadersTransition()
+ && !fragment.getHeadersSupportFragment().isScrolling();
+ }
+ });
+ }
+
+ public static class F_defaultAlignment extends RowsSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void defaultAlignment() throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
+
+ final Rect rect = new Rect();
+
+ final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+ rect.set(0, 0, row0.getWidth(), row0.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row0, rect);
+ assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(gridView);
+ View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+ rect.set(0, 0, row1.getWidth(), row1.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row1, rect);
+ assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+ }
+
+ public static class F_selectBeforeSetAdapter extends RowsSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setSelectedPosition(7, false);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectBeforeSetAdapter() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+ final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ public static class F_selectBeforeAddData extends RowsSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ setSelectedPosition(7, false);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ loadData(adapter, 10, 1);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectBeforeAddData() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+ final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ public static class F_selectAfterAddData extends RowsSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setSelectedPosition(7, false);
+ }
+ }, 1000);
+ }
+ }
+
+ @Test
+ public void selectAfterAddData() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+ final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+ .getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+ }
+
+ static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+ public static class F_restoreSelection extends RowsSupportFragment {
+ public F_restoreSelection() {
+ sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ if (savedInstanceState == null) {
+ setSelectedPosition(7, false);
+ }
+ }
+ }
+
+ @Test
+ public void restoreSelection() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ }
+ );
+ SystemClock.sleep(1000);
+
+ // mActivity is invalid after recreate(), a new Activity instance is created
+ // but we could get Fragment from static variable.
+ RowsSupportFragment fragment = sLastF_restoreSelection.get();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ assertEquals(7, gridView.getSelectedPosition());
+ assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+ }
+
+ public static class F_ListRowWithOnClick extends RowsSupportFragment {
+ Presenter.ViewHolder mLastClickedItemViewHolder;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setOnItemViewClickedListener(new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ mLastClickedItemViewHolder = itemViewHolder;
+ }
+ });
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void prefetchChildItemsBeforeAttach() throws Throwable {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+ F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+ final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ gridView.setSelectedPositionSmooth(lastRowPos);
+ }
+ }
+ );
+ waitForScrollIdle(gridView);
+ ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+ RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+ final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+ prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+ fragment.mLastClickedItemViewHolder = null;
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+ }
+ }
+ );
+ assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+ }
+
+ @Test
+ public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(RowsSupportFragment.class, 2000);
+ final RowsSupportFragment fragment = (RowsSupportFragment) activity.getTestFragment();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ ObjectAdapter adapter = new ObjectAdapter() {
+ @Override
+ public int size() {
+ return 0;
+ }
+
+ @Override
+ public Object get(int position) {
+ return null;
+ }
+
+ @Override
+ public long getId(int position) {
+ return 1;
+ }
+ };
+ adapter.setHasStableIds(true);
+ fragment.setAdapter(adapter);
+ }
+ }
+ );
+ }
+
+ static class StableIdAdapter extends ObjectAdapter {
+ ArrayList<Integer> mList = new ArrayList();
+
+ @Override
+ public long getId(int position) {
+ return mList.get(position).longValue();
+ }
+
+ @Override
+ public Object get(int position) {
+ return mList.get(position);
+ }
+
+ @Override
+ public int size() {
+ return mList.size();
+ }
+ }
+
+ public static class F_rowNotifyItemRangeChange extends BrowseSupportFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 2; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(true);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ setAdapter(adapter);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ StableIdAdapter rowAdapter = (StableIdAdapter)
+ ((ListRow) adapter.get(1)).getAdapter();
+ rowAdapter.notifyItemRangeChanged(0, 3);
+ }
+ }, 500);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void rowNotifyItemRangeChange() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
+
+ VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
+ .getRowsSupportFragment().getVerticalGridView();
+ for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+ HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+ .findViewById(R.id.row_content);
+ for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+ assertEquals(horizontalGridView.getPaddingTop(),
+ horizontalGridView.getChildAt(j).getTop());
+ }
+ }
+ }
+
+ public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseSupportFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListRowPresenter lrp = new ListRowPresenter();
+ prepareEntranceTransition();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 2; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(true);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ setAdapter(adapter);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ StableIdAdapter rowAdapter = (StableIdAdapter)
+ ((ListRow) adapter.get(1)).getAdapter();
+ rowAdapter.notifyItemRangeChanged(0, 3);
+ }
+ }, 500);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ startEntranceTransition();
+ }
+ }, 520);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
+
+ VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
+ .getRowsSupportFragment().getVerticalGridView();
+ for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+ HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+ .findViewById(R.id.row_content);
+ for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+ assertEquals(horizontalGridView.getPaddingTop(),
+ horizontalGridView.getChildAt(j).getTop());
+ assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
+ }
+ }
+ }
+
+ public static class F_Base extends BrowseSupportFragment {
+
+ List<Long> mEntranceTransitionStartTS = new ArrayList();
+ List<Long> mEntranceTransitionEndTS = new ArrayList();
+
+ @Override
+ protected void onEntranceTransitionStart() {
+ super.onEntranceTransitionStart();
+ mEntranceTransitionStartTS.add(SystemClock.uptimeMillis());
+ }
+
+ @Override
+ protected void onEntranceTransitionEnd() {
+ super.onEntranceTransitionEnd();
+ mEntranceTransitionEndTS.add(SystemClock.uptimeMillis());
+ }
+
+ public void assertExecutedEntranceTransition() {
+ assertEquals(1, mEntranceTransitionStartTS.size());
+ assertEquals(1, mEntranceTransitionEndTS.size());
+ assertTrue(mEntranceTransitionEndTS.get(0) - mEntranceTransitionStartTS.get(0) > 100);
+ }
+
+ public void assertNoEntranceTransition() {
+ assertEquals(0, mEntranceTransitionStartTS.size());
+ assertEquals(0, mEntranceTransitionEndTS.size());
+ }
+
+ /**
+ * Util to wait PageFragment swapped.
+ */
+ Fragment waitPageFragment(final Class pageFragmentClass) {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return pageFragmentClass.isInstance(getMainFragment())
+ && getMainFragment().getView() != null;
+ }
+ });
+ return getMainFragment();
+ }
+
+ /**
+ * Wait until a fragment for non-page Row is created. Does not apply to the case a
+ * RowsSupportFragment is created on a PageRow.
+ */
+ RowsSupportFragment waitRowsSupportFragment() {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mMainFragmentListRowDataAdapter != null
+ && getMainFragment() instanceof RowsSupportFragment
+ && !(getMainFragment() instanceof SampleRowsSupportFragment);
+ }
+ });
+ return (RowsSupportFragment) getMainFragment();
+ }
+ }
+
+ static ObjectAdapter createListRowAdapter() {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ return listRowAdapter;
+ }
+
+ /**
+ * Create BrowseSupportFragmentAdapter with 3 ListRows
+ */
+ static ArrayObjectAdapter createListRowsAdapter() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 3; i++) {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ return adapter;
+ }
+
+ /**
+ * A typical BrowseSupportFragment with multiple rows that start entrance transition
+ */
+ public static class F_standard extends F_Base {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(createListRowsAdapter());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentSetNullAdapter() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(null);
+ }
+ });
+ // adapter should no longer has observer and there is no reference to adapter from
+ // BrowseSupportFragment.
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertNull(fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ // RowsSupportFragment is still there
+ assertTrue(fragment.mMainFragment instanceof RowsSupportFragment);
+ assertNotNull(fragment.mMainFragmentRowsAdapter);
+ assertNotNull(fragment.mMainFragmentAdapter);
+
+ // initialize to same adapter
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter1);
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapter() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ final ObjectAdapter adapter2 = createListRowsAdapter();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ final ObjectAdapter adapter2 = create2PageRow3ListRow();
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitPageFragment(SampleRowsSupportFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertFalse(wrappedAdapter.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangeListRowToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new MyPageRow(0));
+ }
+ });
+ fragment.waitPageFragment(SampleRowsSupportFragment.class);
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangeListRowToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.replace(0, new MyPageRow(0));
+ }
+ });
+ fragment.waitPageFragment(SampleRowsSupportFragment.class);
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangeListRowToListRow() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangeListRowToListRow() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+
+ fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+ new MyFragmentFactory());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.replace(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ assertTrue(adapter1.hasObserver());
+ assertTrue(wrappedAdapter.hasObserver());
+ assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterPageToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ final ObjectAdapter adapter2 = create2PageRow3ListRow();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitPageFragment(SampleRowsSupportFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter2.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyChangePageToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new MyPageRow(1));
+ }
+ });
+ fragment.waitPageFragment(SampleFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangePageToPage() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ adapter1.replace(0, new MyPageRow(1));
+ }
+ });
+ fragment.waitPageFragment(SampleFragment.class);
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertTrue(adapter1.hasObserver());
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentChangeAdapterPageToListRow() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ObjectAdapter adapter1 = fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ final ObjectAdapter adapter2 = createListRowsAdapter();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setAdapter(adapter2);
+ }
+ });
+ fragment.waitRowsSupportFragment();
+ // adapter1 should no longer has observer and adapter2 will have observer
+ assertFalse(adapter1.hasObserver());
+ assertSame(adapter2, fragment.getAdapter());
+ assertTrue(adapter2.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyDataChangePageToListRow() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.removeItems(0, 1);
+ adapter1.add(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ fragment.waitRowsSupportFragment();
+ assertTrue(adapter1.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentNotifyItemChangePageToListRow() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+ final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+ assertNull(fragment.mMainFragmentListRowDataAdapter);
+ assertTrue(adapter1.hasObserver());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ ObjectAdapter listRowAdapter = createListRowAdapter();
+ HeaderItem header = new HeaderItem(0, "Row 0 changed");
+ adapter1.replace(0, new ListRow(header, listRowAdapter));
+ }
+ });
+ fragment.waitRowsSupportFragment();
+ assertTrue(adapter1.hasObserver());
+ assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void browseFragmentRestore() throws InterruptedException {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class, 2000);
+ final F_standard fragment = ((F_standard) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select item 2 on row 1
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
+ // save activity to state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ // recreate activity with saved state
+ SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
+ // validate restored activity selected row and selected item
+ fragment2.assertNoEntranceTransition();
+ assertEquals(1, fragment2.getSelectedPosition());
+ assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+ .getSelectedPosition());
+ activity2.finish();
+ }
+
+ public static class MyPageRow extends PageRow {
+ public int type;
+ public MyPageRow(int type) {
+ super(new HeaderItem(100 + type, "page type " + type));
+ this.type = type;
+ }
+ }
+
+ /**
+ * A RowsSupportFragment that is a separate page in BrowseSupportFragment.
+ */
+ public static class SampleRowsSupportFragment extends RowsSupportFragment {
+ public SampleRowsSupportFragment() {
+ // simulates late data loading:
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(createListRowsAdapter());
+ if (getMainFragmentAdapter() != null) {
+ getMainFragmentAdapter().getFragmentHost()
+ .notifyDataReady(getMainFragmentAdapter());
+ }
+ }
+ }, 500);
+ }
+ }
+
+ /**
+ * A custom Fragment that is a separate page in BrowseSupportFragment.
+ */
+ public static class SampleFragment extends Fragment implements
+ BrowseSupportFragment.MainFragmentAdapterProvider {
+
+ public static class PageFragmentAdapterImpl extends
+ BrowseSupportFragment.MainFragmentAdapter<SampleFragment> {
+
+ public PageFragmentAdapterImpl(SampleFragment fragment) {
+ super(fragment);
+ setScalingEnabled(true);
+ }
+
+ @Override
+ public void setEntranceTransitionState(boolean state) {
+ getFragment().setEntranceTransitionState(state);
+ }
+ }
+
+ final PageFragmentAdapterImpl mMainFragmentAdapter = new PageFragmentAdapterImpl(this);
+
+ void setEntranceTransitionState(boolean state) {
+ final View view = getView();
+ int visibility = state ? View.VISIBLE : View.INVISIBLE;
+ view.findViewById(R.id.tv1).setVisibility(visibility);
+ view.findViewById(R.id.tv2).setVisibility(visibility);
+ view.findViewById(R.id.tv3).setVisibility(visibility);
+ }
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.page_fragment, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ // static layout has view and data ready immediately
+ mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
+ mMainFragmentAdapter.getFragmentHost().notifyDataReady(mMainFragmentAdapter);
+ }
+
+ @Override
+ public BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter() {
+ return mMainFragmentAdapter;
+ }
+ }
+
+ /**
+ * Create BrowseSupportFragmentAdapter with 3 ListRows and 2 PageRows
+ */
+ private static ArrayObjectAdapter create3ListRow2PageRowAdapter() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ for (int i = 0; i < 3; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ adapter.add(new MyPageRow(0));
+ adapter.add(new MyPageRow(1));
+ return adapter;
+ }
+
+ /**
+ * Create BrowseSupportFragmentAdapter with 2 PageRows then 3 ListRow
+ */
+ private static ArrayObjectAdapter create2PageRow3ListRow() {
+ ListRowPresenter lrp = new ListRowPresenter();
+ final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ adapter.add(new MyPageRow(0));
+ adapter.add(new MyPageRow(1));
+ for (int i = 0; i < 3; i++) {
+ StableIdAdapter listRowAdapter = new StableIdAdapter();
+ listRowAdapter.setHasStableIds(false);
+ listRowAdapter.setPresenterSelector(
+ new SinglePresenterSelector(sCardPresenter));
+ int index = 0;
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ listRowAdapter.mList.add(index++);
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ return adapter;
+ }
+
+ static class MyFragmentFactory extends BrowseSupportFragment.FragmentFactory {
+ @Override
+ public Fragment createFragment(Object rowObj) {
+ MyPageRow row = (MyPageRow) rowObj;
+ if (row.type == 0) {
+ return new SampleRowsSupportFragment();
+ } else if (row.type == 1) {
+ return new SampleFragment();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A BrowseSupportFragment with three ListRows, one SampleRowsSupportFragment and one SampleFragment.
+ */
+ public static class F_3ListRow2PageRow extends F_Base {
+ public F_3ListRow2PageRow() {
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(create3ListRow2PageRowAdapter());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ /**
+ * A BrowseSupportFragment with three ListRows, one SampleRowsSupportFragment and one SampleFragment.
+ */
+ public static class F_2PageRow3ListRow extends F_Base {
+ public F_2PageRow3ListRow() {
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setAdapter(create2PageRow3ListRow());
+ startEntranceTransition();
+ }
+ }, 100);
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseSupportFragmentRestoreToListRow() throws Throwable {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select item 2 on row 1.
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ // start a new activity with the state
+ SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_standard.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ assertFalse(fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ assertEquals(1, fragment2.getSelectedPosition());
+ assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+ .getSelectedPosition());
+ activity2.finish();
+ }
+
+ void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(final boolean hideFastLane)
+ throws Throwable {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select row 3 which is mapped to SampleRowsSupportFragment.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(3, true);
+ }
+ });
+ // Wait SampleRowsSupportFragment being created
+ final SampleRowsSupportFragment mainFragment = (SampleRowsSupportFragment) fragment.waitPageFragment(
+ SampleRowsSupportFragment.class);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ if (hideFastLane) {
+ fragment.startHeadersTransition(false);
+ }
+ }
+ });
+ // Wait header transition finishes
+ waitForHeaderTransition(fragment);
+ // Select item 1 on row 1 in SampleRowsSupportFragment
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mainFragment.setSelectedPosition(1, true,
+ new ListRowPresenter.SelectItemViewHolderTask(1));
+ }
+ });
+ // Save activity state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_3ListRow2PageRow.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ final SampleRowsSupportFragment mainFragment2 = (SampleRowsSupportFragment) fragment2.waitPageFragment(
+ SampleRowsSupportFragment.class);
+ assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ // Validate BrowseSupportFragment selected row 3 (mapped to SampleRowsSupportFragment)
+ assertEquals(3, fragment2.getSelectedPosition());
+ // Validate SampleRowsSupportFragment's selected row and selected item
+ assertEquals(1, mainFragment2.getSelectedPosition());
+ assertEquals(1, ((ListRowPresenter.ViewHolder) mainFragment2
+ .findRowViewHolderByPosition(1)).getSelectedPosition());
+ activity2.finish();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragmentHideFastLane() throws Throwable {
+ mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(true);
+
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragmentShowFastLane() throws Throwable {
+ mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(false);
+ }
+
+ void mixedBrowseSupportFragmentRestoreToSampleFragment(final boolean hideFastLane)
+ throws Throwable {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+ final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+ fragment.assertExecutedEntranceTransition();
+
+ // select row 3 which is mapped to SampleFragment.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ fragment.setSelectedPosition(4, true);
+ }
+ });
+ // Wait SampleFragment to be created
+ final SampleFragment mainFragment = (SampleFragment) fragment.waitPageFragment(
+ SampleFragment.class);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ if (hideFastLane) {
+ fragment.startHeadersTransition(false);
+ }
+ }
+ });
+ waitForHeaderTransition(fragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ // change TextView content which should be saved in states.
+ TextView t = mainFragment.getView().findViewById(R.id.tv2);
+ t.setText("changed text");
+ }
+ });
+ // Save activity state
+ Bundle savedState = saveActivityState(activity);
+ activity.finish();
+
+ SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+ RowsSupportFragmentTest.F_3ListRow2PageRow.class,
+ new Options().savedInstance(savedState), 2000);
+ final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+ final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
+ SampleFragment.class);
+ assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+ fragment2.assertNoEntranceTransition();
+ // Validate BrowseSupportFragment selected row 3 (mapped to SampleFragment)
+ assertEquals(4, fragment2.getSelectedPosition());
+ // Validate SampleFragment's view states are restored
+ TextView t = mainFragment2.getView().findViewById(R.id.tv2);
+ assertEquals("changed text", t.getText().toString());
+ activity2.finish();
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseSupportFragmentRestoreToSampleFragmentHideFastLane() throws Throwable {
+ mixedBrowseSupportFragmentRestoreToSampleFragment(true);
+
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void mixedBrowseSupportFragmentRestoreToSampleFragmentShowFastLane() throws Throwable {
+ mixedBrowseSupportFragmentRestoreToSampleFragment(false);
+ }
+
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
new file mode 100644
index 0000000..6596daa
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -0,0 +1,97 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleSupportFragmentTestActivity.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.app.Fragment;
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.util.Log;
+
+public class SingleFragmentTestActivity extends Activity {
+
+ /**
+ * Fragment that will be added to activity
+ */
+ public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+
+ public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
+ public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+ public static final String EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE =
+ "overriddenSavedInstanceState";
+
+ private static final String TAG = "TestActivity";
+
+ private Bundle overrideSavedInstance(Bundle savedInstance) {
+ Intent intent = getIntent();
+ if (intent != null) {
+ Bundle b = intent.getBundleExtra(EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE);
+ if (b != null) {
+ return b;
+ }
+ }
+ return savedInstance;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ savedInstanceState = overrideSavedInstance(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate " + this);
+ Intent intent = getIntent();
+
+ final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
+ if (uiOptions != 0) {
+ getWindow().getDecorView().setSystemUiVisibility(uiOptions);
+ }
+
+ setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+ if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
+ try {
+ Fragment fragment = (Fragment) Class.forName(
+ intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ ft.commit();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(overrideSavedInstance(savedInstanceState));
+ }
+
+ public Bundle performSaveInstanceState() {
+ Bundle state = new Bundle();
+ onSaveInstanceState(state);
+ return state;
+ }
+
+ public Fragment getTestFragment() {
+ return getFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
new file mode 100644
index 0000000..150ccae
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -0,0 +1,133 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleSupportFrgamentTestBase.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v7.widget.RecyclerView;
+
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+public class SingleFragmentTestBase {
+
+ private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+
+ @Rule
+ public TestName mUnitTestName = new TestName();
+
+ @Rule
+ public ActivityTestRule<SingleFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(SingleFragmentTestActivity.class, false, false);
+
+ public void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ /**
+ * Options that will be passed throught Intent to SingleFragmentTestActivity
+ */
+ public static class Options {
+ int mActivityLayoutId;
+ int mUiVisibility;
+ Bundle mSavedInstance;
+
+ public Options() {
+ }
+
+ public Options activityLayoutId(int activityLayoutId) {
+ mActivityLayoutId = activityLayoutId;
+ return this;
+ }
+
+ public Options uiVisibility(int uiVisibility) {
+ mUiVisibility = uiVisibility;
+ return this;
+ }
+
+ public Options savedInstance(Bundle savedInstance) {
+ mSavedInstance = savedInstance;
+ return this;
+ }
+
+ public void collect(Intent intent) {
+ if (mActivityLayoutId != 0) {
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
+ mActivityLayoutId);
+ }
+ if (mUiVisibility != 0) {
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
+ }
+ if (mSavedInstance != null) {
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE,
+ mSavedInstance);
+ }
+ }
+ }
+
+ public SingleFragmentTestActivity launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
+ return launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
+ }
+
+ public SingleFragmentTestActivity launchAndWaitActivity(Class fragmentClass, Options options,
+ long waitTimeMs) {
+ return launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
+ }
+
+ public SingleFragmentTestActivity launchAndWaitActivity(String firstFragmentName,
+ Options options, long waitTimeMs) {
+ Intent intent = new Intent();
+ intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+ if (options != null) {
+ options.collect(intent);
+ }
+ SingleFragmentTestActivity activity = activityTestRule.launchActivity(intent);
+ SystemClock.sleep(waitTimeMs);
+ return activity;
+ }
+
+ protected void waitForScrollIdle(RecyclerView recyclerView) throws Throwable {
+ waitForScrollIdle(recyclerView, null);
+ }
+
+ protected void waitForScrollIdle(RecyclerView recyclerView, Runnable verify) throws Throwable {
+ Thread.sleep(100);
+ int total = 0;
+ while (recyclerView.getLayoutManager().isSmoothScrolling()
+ || recyclerView.getScrollState() != recyclerView.SCROLL_STATE_IDLE) {
+ if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
+ throw new RuntimeException("waitForScrollIdle Timeout");
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ break;
+ }
+ if (verify != null) {
+ activityTestRule.runOnUiThread(verify);
+ }
+ }
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
new file mode 100644
index 0000000..eeb6262
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+
+public class SingleSupportFragmentTestActivity extends FragmentActivity {
+
+ /**
+ * Fragment that will be added to activity
+ */
+ public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
+
+ public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
+ public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+ public static final String EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE =
+ "overriddenSavedInstanceState";
+
+ private static final String TAG = "TestActivity";
+
+ private Bundle overrideSavedInstance(Bundle savedInstance) {
+ Intent intent = getIntent();
+ if (intent != null) {
+ Bundle b = intent.getBundleExtra(EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE);
+ if (b != null) {
+ return b;
+ }
+ }
+ return savedInstance;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ savedInstanceState = overrideSavedInstance(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate " + this);
+ Intent intent = getIntent();
+
+ final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
+ if (uiOptions != 0) {
+ getWindow().getDecorView().setSystemUiVisibility(uiOptions);
+ }
+
+ setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+ if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
+ try {
+ Fragment fragment = (Fragment) Class.forName(
+ intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ ft.commit();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ finish();
+ }
+ }
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(overrideSavedInstance(savedInstanceState));
+ }
+
+ public Bundle performSaveInstanceState() {
+ Bundle state = new Bundle();
+ onSaveInstanceState(state);
+ return state;
+ }
+
+ public Fragment getTestFragment() {
+ return getSupportFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
new file mode 100644
index 0000000..8cce627
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v7.widget.RecyclerView;
+
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+public class SingleSupportFragmentTestBase {
+
+ private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+
+ @Rule
+ public TestName mUnitTestName = new TestName();
+
+ @Rule
+ public ActivityTestRule<SingleSupportFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(SingleSupportFragmentTestActivity.class, false, false);
+
+ public void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ /**
+ * Options that will be passed throught Intent to SingleSupportFragmentTestActivity
+ */
+ public static class Options {
+ int mActivityLayoutId;
+ int mUiVisibility;
+ Bundle mSavedInstance;
+
+ public Options() {
+ }
+
+ public Options activityLayoutId(int activityLayoutId) {
+ mActivityLayoutId = activityLayoutId;
+ return this;
+ }
+
+ public Options uiVisibility(int uiVisibility) {
+ mUiVisibility = uiVisibility;
+ return this;
+ }
+
+ public Options savedInstance(Bundle savedInstance) {
+ mSavedInstance = savedInstance;
+ return this;
+ }
+
+ public void collect(Intent intent) {
+ if (mActivityLayoutId != 0) {
+ intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
+ mActivityLayoutId);
+ }
+ if (mUiVisibility != 0) {
+ intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
+ }
+ if (mSavedInstance != null) {
+ intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE,
+ mSavedInstance);
+ }
+ }
+ }
+
+ public SingleSupportFragmentTestActivity launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
+ return launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
+ }
+
+ public SingleSupportFragmentTestActivity launchAndWaitActivity(Class fragmentClass, Options options,
+ long waitTimeMs) {
+ return launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
+ }
+
+ public SingleSupportFragmentTestActivity launchAndWaitActivity(String firstFragmentName,
+ Options options, long waitTimeMs) {
+ Intent intent = new Intent();
+ intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+ if (options != null) {
+ options.collect(intent);
+ }
+ SingleSupportFragmentTestActivity activity = activityTestRule.launchActivity(intent);
+ SystemClock.sleep(waitTimeMs);
+ return activity;
+ }
+
+ protected void waitForScrollIdle(RecyclerView recyclerView) throws Throwable {
+ waitForScrollIdle(recyclerView, null);
+ }
+
+ protected void waitForScrollIdle(RecyclerView recyclerView, Runnable verify) throws Throwable {
+ Thread.sleep(100);
+ int total = 0;
+ while (recyclerView.getLayoutManager().isSmoothScrolling()
+ || recyclerView.getScrollState() != recyclerView.SCROLL_STATE_IDLE) {
+ if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
+ throw new RuntimeException("waitForScrollIdle Timeout");
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ break;
+ }
+ if (verify != null) {
+ activityTestRule.runOnUiThread(verify);
+ }
+ }
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java b/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
rename to leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
diff --git a/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
new file mode 100644
index 0000000..649689c
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
@@ -0,0 +1,71 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VerticalGridSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridPresenter;
+import android.app.Fragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class VerticalGridFragmentTest extends SingleFragmentTestBase {
+
+ public static class GridFragment extends VerticalGridFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
+ gridPresenter.setNumberOfColumns(3);
+ setGridPresenter(gridPresenter);
+ setAdapter(new ArrayObjectAdapter());
+ }
+ }
+
+ @Test
+ public void immediateRemoveFragment() throws Throwable {
+ final SingleFragmentTestActivity activity = launchAndWaitActivity(GridFragment.class, 500);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ GridFragment f = new GridFragment();
+ activity.getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, f, null).commit();
+ f.startEntranceTransition();
+ activity.getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new Fragment(), null).commit();
+ }
+ });
+
+ Thread.sleep(1000);
+ activity.finish();
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
new file mode 100644
index 0000000..ccbfa04
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridPresenter;
+import android.support.v4.app.Fragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ public static class GridFragment extends VerticalGridSupportFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ prepareEntranceTransition();
+ }
+ VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
+ gridPresenter.setNumberOfColumns(3);
+ setGridPresenter(gridPresenter);
+ setAdapter(new ArrayObjectAdapter());
+ }
+ }
+
+ @Test
+ public void immediateRemoveFragment() throws Throwable {
+ final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(GridFragment.class, 500);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ GridFragment f = new GridFragment();
+ activity.getSupportFragmentManager().beginTransaction()
+ .replace(android.R.id.content, f, null).commit();
+ f.startEntranceTransition();
+ activity.getSupportFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new Fragment(), null).commit();
+ }
+ });
+
+ Thread.sleep(1000);
+ activity.finish();
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
new file mode 100644
index 0000000..a8b65d8
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
@@ -0,0 +1,256 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class VideoFragmentTest extends SingleFragmentTestBase {
+
+ public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoFragment {
+ boolean mSurfaceCreated;
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreated = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceCreated = false;
+ }
+ });
+
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+ }
+
+ @Test
+ public void setSurfaceViewCallbackBeforeCreate() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
+ assertNotNull(fragment1);
+ assertTrue(fragment1.mSurfaceCreated);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ activity.getFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+ .commitAllowingStateLoss();
+ }
+ });
+ SystemClock.sleep(500);
+
+ assertFalse(fragment1.mSurfaceCreated);
+
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
+ assertNotNull(fragment2);
+ assertTrue(fragment2.mSurfaceCreated);
+ assertNotSame(fragment1, fragment2);
+ }
+
+ @Test
+ public void setSurfaceViewCallbackAfterCreate() {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(VideoFragment.class, 1000);
+ VideoFragment fragment = (VideoFragment) activity.getTestFragment();
+
+ assertNotNull(fragment);
+
+ final boolean[] surfaceCreated = new boolean[1];
+ fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ surfaceCreated[0] = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ surfaceCreated[0] = false;
+ }
+ });
+ assertTrue(surfaceCreated[0]);
+ }
+
+ public static class Fragment_withVideoPlayer extends VideoFragment {
+ MediaPlayerGlue mGlue;
+ int mOnCreateCalled;
+ int mOnCreateViewCalled;
+ int mOnDestroyViewCalled;
+ int mOnDestroyCalled;
+ int mGlueAttachedToHost;
+ int mGlueDetachedFromHost;
+ int mGlueOnReadyForPlaybackCalled;
+
+ public Fragment_withVideoPlayer() {
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mOnCreateCalled++;
+ super.onCreate(savedInstanceState);
+ mGlue = new MediaPlayerGlue(getActivity()) {
+ @Override
+ protected void onDetachedFromHost() {
+ mGlueDetachedFromHost++;
+ super.onDetachedFromHost();
+ }
+
+ @Override
+ protected void onAttachedToHost(PlaybackGlueHost host) {
+ super.onAttachedToHost(host);
+ mGlueAttachedToHost++;
+ }
+ };
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("Leanback");
+ mGlue.setTitle("Leanback team at work");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onPreparedStateChanged(PlaybackGlue glue) {
+ if (glue.isPrepared()) {
+ mGlueOnReadyForPlaybackCalled++;
+ mGlue.play();
+ }
+ }
+ });
+ mGlue.setHost(new VideoFragmentGlueHost(this));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mOnCreateViewCalled++;
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mOnDestroyViewCalled++;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mOnDestroyCalled++;
+ super.onDestroy();
+ }
+ }
+
+ @Test
+ public void mediaPlayerGlueInVideoFragment() {
+ final SingleFragmentTestActivity activity =
+ launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+ final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+ activity.getTestFragment();
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(1, fragment.mOnCreateViewCalled);
+ assertEquals(0, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+ View fragmentViewBeforeRecreate = fragment.getView();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+ }
+ });
+ View fragmentViewAfterRecreate = fragment.getView();
+
+ Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(2, fragment.mOnCreateViewCalled);
+ assertEquals(1, fragment.mOnDestroyViewCalled);
+
+ assertEquals(1, fragment.mGlueAttachedToHost);
+ assertEquals(0, fragment.mGlueDetachedFromHost);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+ activity.finish();
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlueDetachedFromHost == 1;
+ }
+ });
+ assertEquals(2, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mOnDestroyCalled);
+ }
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
new file mode 100644
index 0000000..4d66285
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class VideoSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoSupportFragment {
+ boolean mSurfaceCreated;
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceCreated = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceCreated = false;
+ }
+ });
+
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+ }
+
+ @Test
+ public void setSurfaceViewCallbackBeforeCreate() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
+ assertNotNull(fragment1);
+ assertTrue(fragment1.mSurfaceCreated);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ activity.getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+ .commitAllowingStateLoss();
+ }
+ });
+ SystemClock.sleep(500);
+
+ assertFalse(fragment1.mSurfaceCreated);
+
+ Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+ (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
+ assertNotNull(fragment2);
+ assertTrue(fragment2.mSurfaceCreated);
+ assertNotSame(fragment1, fragment2);
+ }
+
+ @Test
+ public void setSurfaceViewCallbackAfterCreate() {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(VideoSupportFragment.class, 1000);
+ VideoSupportFragment fragment = (VideoSupportFragment) activity.getTestFragment();
+
+ assertNotNull(fragment);
+
+ final boolean[] surfaceCreated = new boolean[1];
+ fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ surfaceCreated[0] = true;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ surfaceCreated[0] = false;
+ }
+ });
+ assertTrue(surfaceCreated[0]);
+ }
+
+ public static class Fragment_withVideoPlayer extends VideoSupportFragment {
+ MediaPlayerGlue mGlue;
+ int mOnCreateCalled;
+ int mOnCreateViewCalled;
+ int mOnDestroyViewCalled;
+ int mOnDestroyCalled;
+ int mGlueAttachedToHost;
+ int mGlueDetachedFromHost;
+ int mGlueOnReadyForPlaybackCalled;
+
+ public Fragment_withVideoPlayer() {
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mOnCreateCalled++;
+ super.onCreate(savedInstanceState);
+ mGlue = new MediaPlayerGlue(getActivity()) {
+ @Override
+ protected void onDetachedFromHost() {
+ mGlueDetachedFromHost++;
+ super.onDetachedFromHost();
+ }
+
+ @Override
+ protected void onAttachedToHost(PlaybackGlueHost host) {
+ super.onAttachedToHost(host);
+ mGlueAttachedToHost++;
+ }
+ };
+ mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mGlue.setArtist("Leanback");
+ mGlue.setTitle("Leanback team at work");
+ mGlue.setMediaSource(
+ Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+ mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
+ @Override
+ public void onPreparedStateChanged(PlaybackGlue glue) {
+ if (glue.isPrepared()) {
+ mGlueOnReadyForPlaybackCalled++;
+ mGlue.play();
+ }
+ }
+ });
+ mGlue.setHost(new VideoSupportFragmentGlueHost(this));
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mOnCreateViewCalled++;
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mOnDestroyViewCalled++;
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ mOnDestroyCalled++;
+ super.onDestroy();
+ }
+ }
+
+ @Test
+ public void mediaPlayerGlueInVideoSupportFragment() {
+ final SingleSupportFragmentTestActivity activity =
+ launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+ final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+ activity.getTestFragment();
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlue.isMediaPlaying();
+ }
+ });
+
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(1, fragment.mOnCreateViewCalled);
+ assertEquals(0, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+ View fragmentViewBeforeRecreate = fragment.getView();
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+ }
+ });
+ View fragmentViewAfterRecreate = fragment.getView();
+
+ Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+ assertEquals(1, fragment.mOnCreateCalled);
+ assertEquals(2, fragment.mOnCreateViewCalled);
+ assertEquals(1, fragment.mOnDestroyViewCalled);
+
+ assertEquals(1, fragment.mGlueAttachedToHost);
+ assertEquals(0, fragment.mGlueDetachedFromHost);
+ assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+ activity.finish();
+ PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return fragment.mGlueDetachedFromHost == 1;
+ }
+ });
+ assertEquals(2, fragment.mOnDestroyViewCalled);
+ assertEquals(1, fragment.mOnDestroyCalled);
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java b/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
rename to leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java b/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
rename to leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java b/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
rename to leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java b/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
rename to leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
diff --git a/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
new file mode 100644
index 0000000..7bf96a5
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -0,0 +1,5747 @@
+/*
+ * 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 android.support.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
+import android.os.Parcelable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import android.text.Selection;
+import android.text.Spannable;
+import android.util.DisplayMetrics;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class GridWidgetTest {
+
+ private static final float DELTA = 1f;
+ private static final boolean HUMAN_DELAY = false;
+ private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+ private static final int WAIT_FOR_LAYOUT_PASS_TIMEOUT_MS = 2000;
+ private static final int WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS = 6000;
+
+ protected ActivityTestRule<GridActivity> mActivityTestRule;
+ protected GridActivity mActivity;
+ protected BaseGridView mGridView;
+ protected GridLayoutManager mLayoutManager;
+ private GridLayoutManager.OnLayoutCompleteListener mWaitLayoutListener;
+ protected int mOrientation;
+ protected int mNumRows;
+ protected int[] mRemovedItems;
+
+ private final Comparator<View> mRowSortComparator = new Comparator<View>() {
+ @Override
+ public int compare(View lhs, View rhs) {
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ return lhs.getLeft() - rhs.getLeft();
+ } else {
+ return lhs.getTop() - rhs.getTop();
+ }
+ };
+ };
+
+ /**
+ * Verify margins between items on same row are same.
+ */
+ private final Runnable mVerifyLayout = new Runnable() {
+ @Override
+ public void run() {
+ verifyMargin();
+ }
+ };
+
+ @Rule public TestName testName = new TestName();
+
+ public static void sendKey(int keyCode) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
+ }
+
+ public static void sendRepeatedKeys(int repeats, int keyCode) {
+ for (int i = 0; i < repeats; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
+ }
+ }
+
+ private void humanDelay(int delay) throws InterruptedException {
+ if (HUMAN_DELAY) Thread.sleep(delay);
+ }
+ /**
+ * Change size of the Adapter and notifyDataSetChanged.
+ */
+ private void changeArraySize(final int size) throws Throwable {
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.changeArraySize(size);
+ }
+ });
+ }
+
+ static String dumpGridView(BaseGridView gridView) {
+ return "findFocus:" + gridView.getRootView().findFocus()
+ + " isLayoutRequested:" + gridView.isLayoutRequested()
+ + " selectedPosition:" + gridView.getSelectedPosition()
+ + " adapter.itemCount:" + gridView.getAdapter().getItemCount()
+ + " itemAnimator.isRunning:" + gridView.getItemAnimator().isRunning()
+ + " scrollState:" + gridView.getScrollState();
+ }
+
+ /**
+ * Change selected position.
+ */
+ private void setSelectedPosition(final int position, final int scrollExtra) throws Throwable {
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPosition(position, scrollExtra);
+ }
+ });
+ waitForLayout(false);
+ }
+
+ private void setSelectedPosition(final int position) throws Throwable {
+ setSelectedPosition(position, 0);
+ }
+
+ private void setSelectedPositionSmooth(final int position) throws Throwable {
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(position);
+ }
+ });
+ }
+ /**
+ * Scrolls using given key.
+ */
+ protected void scroll(int key, Runnable verify) throws Throwable {
+ do {
+ if (verify != null) {
+ mActivityTestRule.runOnUiThread(verify);
+ }
+ sendRepeatedKeys(10, key);
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException ex) {
+ break;
+ }
+ } while (mGridView.getLayoutManager().isSmoothScrolling()
+ || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE);
+ }
+
+ protected void scrollToBegin(Runnable verify) throws Throwable {
+ int key;
+ // first move to first column/row
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ key = KeyEvent.KEYCODE_DPAD_UP;
+ } else {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ }
+ scroll(key, null);
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_UP;
+ }
+ scroll(key, verify);
+ }
+
+ protected void scrollToEnd(Runnable verify) throws Throwable {
+ int key;
+ // first move to first column/row
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ key = KeyEvent.KEYCODE_DPAD_UP;
+ } else {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ }
+ scroll(key, null);
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ }
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_DOWN;
+ }
+ scroll(key, verify);
+ }
+
+ /**
+ * Group and sort children by their position on each row (HORIZONTAL) or column(VERTICAL).
+ */
+ protected View[][] sortByRows() {
+ final HashMap<Integer, ArrayList<View>> rows = new HashMap<Integer, ArrayList<View>>();
+ ArrayList<Integer> rowLocations = new ArrayList<>();
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ View v = mGridView.getChildAt(i);
+ int rowLocation;
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ rowLocation = v.getTop();
+ } else {
+ rowLocation = mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL
+ ? v.getRight() : v.getLeft();
+ }
+ ArrayList<View> views = rows.get(rowLocation);
+ if (views == null) {
+ views = new ArrayList<View>();
+ rows.put(rowLocation, views);
+ rowLocations.add(rowLocation);
+ }
+ views.add(v);
+ }
+ Object[] sortedLocations = rowLocations.toArray();
+ Arrays.sort(sortedLocations);
+ if (mNumRows != rows.size()) {
+ assertEquals("Dump Views by rows "+rows, mNumRows, rows.size());
+ }
+ View[][] sorted = new View[rows.size()][];
+ for (int i = 0; i < rowLocations.size(); i++) {
+ Integer rowLocation = rowLocations.get(i);
+ ArrayList<View> arr = rows.get(rowLocation);
+ View[] views = arr.toArray(new View[arr.size()]);
+ Arrays.sort(views, mRowSortComparator);
+ sorted[i] = views;
+ }
+ return sorted;
+ }
+
+ protected void verifyMargin() {
+ View[][] sorted = sortByRows();
+ for (int row = 0; row < sorted.length; row++) {
+ View[] views = sorted[row];
+ int margin = -1;
+ for (int i = 1; i < views.length; i++) {
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ assertEquals(mGridView.getHorizontalMargin(),
+ views[i].getLeft() - views[i - 1].getRight());
+ } else {
+ assertEquals(mGridView.getVerticalMargin(),
+ views[i].getTop() - views[i - 1].getBottom());
+ }
+ }
+ }
+ }
+
+ protected void verifyBeginAligned() {
+ View[][] sorted = sortByRows();
+ int alignedLocation = 0;
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ for (int i = 0; i < sorted.length; i++) {
+ if (i == 0) {
+ alignedLocation = sorted[i][sorted[i].length - 1].getRight();
+ } else {
+ assertEquals(alignedLocation, sorted[i][sorted[i].length - 1].getRight());
+ }
+ }
+ } else {
+ for (int i = 0; i < sorted.length; i++) {
+ if (i == 0) {
+ alignedLocation = sorted[i][0].getLeft();
+ } else {
+ assertEquals(alignedLocation, sorted[i][0].getLeft());
+ }
+ }
+ }
+ } else {
+ for (int i = 0; i < sorted.length; i++) {
+ if (i == 0) {
+ alignedLocation = sorted[i][0].getTop();
+ } else {
+ assertEquals(alignedLocation, sorted[i][0].getTop());
+ }
+ }
+ }
+ }
+
+ protected int[] getEndEdges() {
+ View[][] sorted = sortByRows();
+ int[] edges = new int[sorted.length];
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ for (int i = 0; i < sorted.length; i++) {
+ edges[i] = sorted[i][0].getLeft();
+ }
+ } else {
+ for (int i = 0; i < sorted.length; i++) {
+ edges[i] = sorted[i][sorted[i].length - 1].getRight();
+ }
+ }
+ } else {
+ for (int i = 0; i < sorted.length; i++) {
+ edges[i] = sorted[i][sorted[i].length - 1].getBottom();
+ }
+ }
+ return edges;
+ }
+
+ protected void verifyEdgesSame(int[] edges, int[] edges2) {
+ assertEquals(edges.length, edges2.length);
+ for (int i = 0; i < edges.length; i++) {
+ assertEquals(edges[i], edges2[i]);
+ }
+ }
+
+ protected void verifyBoundCount(int count) {
+ if (mActivity.getBoundCount() != count) {
+ StringBuffer b = new StringBuffer();
+ b.append("ItemsLength: ");
+ for (int i = 0; i < mActivity.mItemLengths.length; i++) {
+ b.append(mActivity.mItemLengths[i]).append(",");
+ }
+ assertEquals("Bound count does not match, ItemsLengths: "+ b,
+ count, mActivity.getBoundCount());
+ }
+ }
+
+ private static int getCenterY(View v) {
+ return (v.getTop() + v.getBottom())/2;
+ }
+
+ private static int getCenterX(View v) {
+ return (v.getLeft() + v.getRight())/2;
+ }
+
+ private void initActivity(Intent intent) throws Throwable {
+ mActivityTestRule = new ActivityTestRule<GridActivity>(GridActivity.class, false, false);
+ mActivity = mActivityTestRule.launchActivity(intent);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.setTitle(testName.getMethodName());
+ }
+ });
+ Thread.sleep(1000);
+ mGridView = mActivity.mGridView;
+ mLayoutManager = (GridLayoutManager) mGridView.getLayoutManager();
+ }
+
+ @After
+ public void clearTest() {
+ mWaitLayoutListener = null;
+ mLayoutManager = null;
+ mGridView = null;
+ mActivity = null;
+ mActivityTestRule = null;
+ }
+
+ /**
+ * Must be called before waitForLayout() to prepare layout listener.
+ */
+ protected void startWaitLayout() {
+ if (mWaitLayoutListener != null) {
+ throw new IllegalStateException("startWaitLayout() already called");
+ }
+ if (mLayoutManager.mLayoutCompleteListener != null) {
+ throw new IllegalStateException("Cannot startWaitLayout()");
+ }
+ mWaitLayoutListener = mLayoutManager.mLayoutCompleteListener =
+ mock(GridLayoutManager.OnLayoutCompleteListener.class);
+ }
+
+ /**
+ * wait layout to be called and remove the listener.
+ */
+ protected void waitForLayout() {
+ waitForLayout(true);
+ }
+
+ /**
+ * wait layout to be called and remove the listener.
+ * @param force True if always wait regardless if layout requested
+ */
+ protected void waitForLayout(boolean force) {
+ if (mWaitLayoutListener == null) {
+ throw new IllegalStateException("startWaitLayout() not called");
+ }
+ if (mWaitLayoutListener != mLayoutManager.mLayoutCompleteListener) {
+ throw new IllegalStateException("layout listener inconistent");
+ }
+ try {
+ if (force || mGridView.isLayoutRequested()) {
+ verify(mWaitLayoutListener, timeout(WAIT_FOR_LAYOUT_PASS_TIMEOUT_MS).atLeastOnce())
+ .onLayoutCompleted(any(RecyclerView.State.class));
+ }
+ } finally {
+ mWaitLayoutListener = null;
+ mLayoutManager.mLayoutCompleteListener = null;
+ }
+ }
+
+ /**
+ * If currently running animator, wait for it to finish, otherwise return immediately.
+ * To wait the ItemAnimator start, you can use waitForLayout() to make sure layout pass has
+ * processed adapter change.
+ */
+ protected void waitForItemAnimation(int timeoutMs) throws Throwable {
+ final RecyclerView.ItemAnimator.ItemAnimatorFinishedListener listener = mock(
+ RecyclerView.ItemAnimator.ItemAnimatorFinishedListener.class);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().isRunning(listener);
+ }
+ });
+ verify(listener, timeout(timeoutMs).atLeastOnce()).onAnimationsFinished();
+ }
+
+ protected void waitForItemAnimation() throws Throwable {
+ waitForItemAnimation(WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS);
+ }
+
+ /**
+ * wait animation start
+ */
+ protected void waitForItemAnimationStart() throws Throwable {
+ long totalWait = 0;
+ while (!mGridView.getItemAnimator().isRunning()) {
+ Thread.sleep(10);
+ if ((totalWait += 10) > WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS) {
+ throw new RuntimeException("waitForItemAnimationStart Timeout");
+ }
+ }
+ }
+
+ /**
+ * Run task in UI thread and wait for layout and ItemAnimator finishes.
+ */
+ protected void performAndWaitForAnimation(Runnable task) throws Throwable {
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(task);
+ waitForLayout();
+ waitForItemAnimation();
+ }
+
+ protected void waitForScrollIdle() throws Throwable {
+ waitForScrollIdle(null);
+ }
+
+ /**
+ * Wait for grid view stop scroll and optionally verify state of grid view.
+ */
+ protected void waitForScrollIdle(Runnable verify) throws Throwable {
+ Thread.sleep(100);
+ int total = 0;
+ while (mGridView.getLayoutManager().isSmoothScrolling()
+ || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
+ if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
+ throw new RuntimeException("waitForScrollIdle Timeout");
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ break;
+ }
+ if (verify != null) {
+ mActivityTestRule.runOnUiThread(verify);
+ }
+ }
+ }
+
+ @Test
+ public void testThreeRowHorizontalBasic() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ scrollToEnd(mVerifyLayout);
+
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+ }
+
+ static class DividerDecoration extends RecyclerView.ItemDecoration {
+
+ private ColorDrawable mTopDivider;
+ private ColorDrawable mBottomDivider;
+ private int mLeftOffset;
+ private int mRightOffset;
+ private int mTopOffset;
+ private int mBottomOffset;
+
+ DividerDecoration(int leftOffset, int topOffset, int rightOffset, int bottomOffset) {
+ mLeftOffset = leftOffset;
+ mTopOffset = topOffset;
+ mRightOffset = rightOffset;
+ mBottomOffset = bottomOffset;
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (mTopDivider == null) {
+ mTopDivider = new ColorDrawable(Color.RED);
+ }
+ if (mBottomDivider == null) {
+ mBottomDivider = new ColorDrawable(Color.BLUE);
+ }
+ final int childCount = parent.getChildCount();
+ final int width = parent.getWidth();
+ for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) {
+ final View view = parent.getChildAt(childViewIndex);
+ mTopDivider.setBounds(0, (int) view.getY() - mTopOffset, width, (int) view.getY());
+ mTopDivider.draw(c);
+ mBottomDivider.setBounds(0, (int) view.getY() + view.getHeight(), width,
+ (int) view.getY() + view.getHeight() + mBottomOffset);
+ mBottomDivider.draw(c);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ outRect.left = mLeftOffset;
+ outRect.top = mTopOffset;
+ outRect.right = mRightOffset;
+ outRect.bottom = mBottomOffset;
+ }
+ }
+
+ @Test
+ public void testItemDecorationAndMargins() throws Throwable {
+
+ final int leftMargin = 3;
+ final int topMargin = 4;
+ final int rightMargin = 7;
+ final int bottomMargin = 8;
+ final int itemHeight = 100;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{itemHeight, itemHeight, itemHeight});
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_MARGINS,
+ new int[]{leftMargin, topMargin, rightMargin, bottomMargin});
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ final int paddingLeft = mGridView.getPaddingLeft();
+ final int paddingTop = mGridView.getPaddingTop();
+ final int verticalSpace = mGridView.getVerticalMargin();
+ final int decorationLeft = 17;
+ final int decorationTop = 1;
+ final int decorationRight = 19;
+ final int decorationBottom = 2;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.addItemDecoration(new DividerDecoration(decorationLeft, decorationTop,
+ decorationRight, decorationBottom));
+ }
+ });
+
+ View child0 = mGridView.getChildAt(0);
+ View child1 = mGridView.getChildAt(1);
+ View child2 = mGridView.getChildAt(2);
+
+ assertEquals(itemHeight, child0.getBottom() - child0.getTop());
+
+ // verify left margins
+ assertEquals(paddingLeft + leftMargin + decorationLeft, child0.getLeft());
+ assertEquals(paddingLeft + leftMargin + decorationLeft, child1.getLeft());
+ assertEquals(paddingLeft + leftMargin + decorationLeft, child2.getLeft());
+ // verify top bottom margins and decoration offset
+ assertEquals(paddingTop + topMargin + decorationTop, child0.getTop());
+ assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
+ child1.getTop() - child0.getBottom());
+ assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
+ child2.getTop() - child1.getBottom());
+
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void testItemDecorationAndMarginsAndOpticalBounds() throws Throwable {
+ final int leftMargin = 3;
+ final int topMargin = 4;
+ final int rightMargin = 7;
+ final int bottomMargin = 8;
+ final int itemHeight = 100;
+ final int ninePatchDrawableResourceId = R.drawable.lb_card_shadow_focused;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{itemHeight, itemHeight, itemHeight});
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_MARGINS,
+ new int[]{leftMargin, topMargin, rightMargin, bottomMargin});
+ intent.putExtra(GridActivity.EXTRA_NINEPATCH_SHADOW, ninePatchDrawableResourceId);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ final int paddingLeft = mGridView.getPaddingLeft();
+ final int paddingTop = mGridView.getPaddingTop();
+ final int verticalSpace = mGridView.getVerticalMargin();
+ final int decorationLeft = 17;
+ final int decorationTop = 1;
+ final int decorationRight = 19;
+ final int decorationBottom = 2;
+
+ final Rect opticalPaddings = new Rect();
+ mGridView.getResources().getDrawable(ninePatchDrawableResourceId)
+ .getPadding(opticalPaddings);
+ final int opticalInsetsLeft = opticalPaddings.left;
+ final int opticalInsetsTop = opticalPaddings.top;
+ final int opticalInsetsRight = opticalPaddings.right;
+ final int opticalInsetsBottom = opticalPaddings.bottom;
+ assertTrue(opticalInsetsLeft > 0);
+ assertTrue(opticalInsetsTop > 0);
+ assertTrue(opticalInsetsRight > 0);
+ assertTrue(opticalInsetsBottom > 0);
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.addItemDecoration(new DividerDecoration(decorationLeft, decorationTop,
+ decorationRight, decorationBottom));
+ }
+ });
+
+ View child0 = mGridView.getChildAt(0);
+ View child1 = mGridView.getChildAt(1);
+ View child2 = mGridView.getChildAt(2);
+
+ assertEquals(itemHeight + opticalInsetsTop + opticalInsetsBottom,
+ child0.getBottom() - child0.getTop());
+
+ // verify left margins decoration and optical insets
+ assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
+ child0.getLeft());
+ assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
+ child1.getLeft());
+ assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
+ child2.getLeft());
+ // verify top bottom margins decoration offset and optical insets
+ assertEquals(paddingTop + topMargin + decorationTop, child0.getTop() + opticalInsetsTop);
+ assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
+ (child1.getTop() + opticalInsetsTop) - (child0.getBottom() - opticalInsetsBottom));
+ assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
+ (child2.getTop() + opticalInsetsTop) - (child1.getBottom() - opticalInsetsBottom));
+
+ }
+
+ @Test
+ public void testThreeColumnVerticalBasic() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ scrollToEnd(mVerifyLayout);
+
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testRedundantAppendRemove() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid_testredundantappendremove);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
+ 149,177,128,234,227,187,163,223,146,210,228,148,227,193,182,197,177,142,225,207,
+ 157,171,209,204,187,184,123,221,197,153,202,179,193,214,226,173,225,143,188,159,
+ 139,193,233,143,227,203,222,124,228,223,164,131,228,126,211,160,165,152,235,184,
+ 155,224,149,181,171,229,200,234,177,130,164,172,188,139,132,203,179,220,147,131,
+ 226,127,230,239,183,203,206,227,123,170,239,234,200,149,237,204,160,133,202,234,
+ 173,122,139,149,151,153,216,231,121,145,227,153,186,174,223,180,123,215,206,216,
+ 239,222,219,207,193,218,140,133,171,153,183,132,233,138,159,174,189,171,143,128,
+ 152,222,141,202,224,190,134,120,181,231,230,136,132,224,136,210,207,150,128,183,
+ 221,194,179,220,126,221,137,205,223,193,172,132,226,209,133,191,227,127,159,171,
+ 180,149,237,177,194,207,170,202,161,144,147,199,205,186,164,140,193,203,224,129});
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ scrollToEnd(mVerifyLayout);
+
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testRedundantAppendRemove2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid_testredundantappendremove2);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
+ 318,333,199,224,246,273,269,289,340,313,265,306,349,269,185,282,257,354,316,252,
+ 237,290,283,343,196,313,290,343,191,262,342,228,343,349,251,203,226,305,265,213,
+ 216,333,295,188,187,281,288,311,244,232,224,332,290,181,267,276,226,261,335,355,
+ 225,217,219,183,234,285,257,304,182,250,244,223,257,219,342,185,347,205,302,315,
+ 299,309,292,237,192,309,228,250,347,227,337,298,299,185,185,331,223,284,265,351});
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+ mLayoutManager = (GridLayoutManager) mGridView.getLayoutManager();
+
+ // test append without staggered result cache
+ scrollToEnd(mVerifyLayout);
+
+ int[] endEdges = getEndEdges();
+
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+
+ // now test append with staggered result cache
+ changeArraySize(3);
+ assertEquals("Staggerd cache should be kept as is when no item size change",
+ 100, ((StaggeredGrid) mLayoutManager.mGrid).mLocations.size());
+
+ changeArraySize(100);
+
+ scrollToEnd(mVerifyLayout);
+
+ // we should get same aligned end edges
+ int[] endEdges2 = getEndEdges();
+ verifyEdgesSame(endEdges, endEdges2);
+ }
+
+
+ @Test
+ public void testLayoutWhenAViewIsInvalidated() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
+ intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, true);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mNumRows = 1;
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ waitOneUiCycle();
+
+ // push views to cache.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.mItemLengths[0] = mActivity.mItemLengths[0] * 3;
+ mActivity.mGridView.getAdapter().notifyItemChanged(0);
+ }
+ });
+ waitForItemAnimation();
+
+ // notifyDataSetChange will mark the cached views FLAG_INVALID
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.mGridView.getAdapter().notifyDataSetChanged();
+ }
+ });
+ waitForItemAnimation();
+
+ // Cached views will be added in prelayout with FLAG_INVALID, in post layout we should
+ // handle it properly
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.mItemLengths[0] = mActivity.mItemLengths[0] / 3;
+ mActivity.mGridView.getAdapter().notifyItemChanged(0);
+ }
+ });
+
+ waitForItemAnimation();
+ }
+
+ @Test
+ public void testWrongInsertViewIndexInFastRelayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mNumRows = 1;
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+
+ // removing two children, they will be hidden views as first 2 children of RV.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setRemoveDuration(2000);
+ mActivity.removeItems(0, 2);
+ }
+ });
+ waitForItemAnimationStart();
+
+ // add three views and notify change of the first item.
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(0, new int[]{161, 161, 161});
+ }
+ });
+ waitForLayout();
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyItemChanged(0);
+ }
+ });
+ waitForLayout();
+ // after layout, the viewholder should still be the first child of LayoutManager.
+ assertEquals(0, mGridView.getChildAdapterPosition(
+ mGridView.getLayoutManager().getChildAt(0)));
+ }
+
+ @Test
+ public void testMoveIntoPrelayoutItems() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mNumRows = 1;
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+
+ final int lastItemPos = mGridView.getChildCount() - 1;
+ assertTrue(mGridView.getChildCount() >= 4);
+ // notify change of 3 items, so prelayout will layout extra 3 items, then move an item
+ // into the extra layout range. Post layout's fastRelayout() should handle this properly.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyItemChanged(lastItemPos - 3);
+ mGridView.getAdapter().notifyItemChanged(lastItemPos - 2);
+ mGridView.getAdapter().notifyItemChanged(lastItemPos - 1);
+ mActivity.moveItem(900, lastItemPos + 2, true);
+ }
+ });
+ waitForItemAnimation();
+ }
+
+ @Test
+ public void testMoveIntoPrelayoutItems2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mNumRows = 1;
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+
+ setSelectedPosition(999);
+ final int firstItemPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(0));
+ assertTrue(mGridView.getChildCount() >= 4);
+ // notify change of 3 items, so prelayout will layout extra 3 items, then move an item
+ // into the extra layout range. Post layout's fastRelayout() should handle this properly.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyItemChanged(firstItemPos + 1);
+ mGridView.getAdapter().notifyItemChanged(firstItemPos + 2);
+ mGridView.getAdapter().notifyItemChanged(firstItemPos + 3);
+ mActivity.moveItem(0, firstItemPos - 2, true);
+ }
+ });
+ waitForItemAnimation();
+ }
+
+ void preparePredictiveLayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setAddDuration(1000);
+ mGridView.getItemAnimator().setRemoveDuration(1000);
+ mGridView.getItemAnimator().setMoveDuration(1000);
+ mGridView.getItemAnimator().setChangeDuration(1000);
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ }
+
+ @Test
+ public void testPredictiveLayoutAdd1() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(51, new int[]{300, 300, 300, 300});
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ assertEquals(50, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutAdd2() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{300, 300, 300, 300});
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ assertEquals(54, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove1() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(51, 3);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ assertEquals(50, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove2() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(47, 3);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ assertEquals(47, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove3() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 51);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ assertEquals(0, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveOnMeasureWrapContent() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_wrap_content);
+ int count = 50;
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, count);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ waitForScrollIdle(mVerifyLayout);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setHasFixedSize(false);
+ }
+ });
+
+ for (int i = 0; i < 30; i++) {
+ final int oldCount = count;
+ final int newCount = i;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (oldCount > 0) {
+ mActivity.removeItems(0, oldCount);
+ }
+ if (newCount > 0) {
+ int[] newItems = new int[newCount];
+ for (int i = 0; i < newCount; i++) {
+ newItems[i] = 400;
+ }
+ mActivity.addItems(0, newItems);
+ }
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation();
+ count = newCount;
+ }
+
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove4() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle();
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 49);
+ }
+ });
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove5() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle();
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(50, 40);
+ }
+ });
+ assertEquals(50, mGridView.getSelectedPosition());
+ scrollToBegin(mVerifyLayout);
+ verifyBeginAligned();
+ }
+
+ void waitOneUiCycle() throws Throwable {
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ }
+ });
+ }
+
+ @Test
+ public void testDontPruneMovingItem() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setMoveDuration(2000);
+ mGridView.setSelectedPosition(50);
+ }
+ });
+ waitForScrollIdle();
+ final ArrayList<RecyclerView.ViewHolder> moveViewHolders = new ArrayList();
+ for (int i = 51;; i++) {
+ RecyclerView.ViewHolder vh = mGridView.findViewHolderForAdapterPosition(i);
+ if (vh == null) {
+ break;
+ }
+ moveViewHolders.add(vh);
+ }
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // add a lot of items, so we will push everything to right of 51 out side window
+ int[] lots_items = new int[1000];
+ for (int i = 0; i < lots_items.length; i++) {
+ lots_items[i] = 300;
+ }
+ mActivity.addItems(51, lots_items);
+ }
+ });
+ waitOneUiCycle();
+ // run a scroll pass, the scroll pass should not remove the animating views even they are
+ // outside visible areas.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollBy(-3, 0);
+ }
+ });
+ waitOneUiCycle();
+ for (int i = 0; i < moveViewHolders.size(); i++) {
+ assertSame(mGridView, moveViewHolders.get(i).itemView.getParent());
+ }
+ }
+
+ @Test
+ public void testMoveItemToTheRight() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setAddDuration(2000);
+ mGridView.getItemAnimator().setMoveDuration(2000);
+ mGridView.setSelectedPosition(50);
+ }
+ });
+ waitForScrollIdle();
+ RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(51);
+
+ int lastPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(
+ mGridView.getChildCount() - 1));
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.moveItem(51, 1000, true);
+ }
+ });
+ final ArrayList<View> moveInViewHolders = new ArrayList();
+ waitForItemAnimationStart();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
+ View v = mGridView.getLayoutManager().getChildAt(i);
+ if (mGridView.getChildAdapterPosition(v) >= 51) {
+ moveInViewHolders.add(v);
+ }
+ }
+ }
+ });
+ waitOneUiCycle();
+ assertTrue("prelayout should layout extra items to slide in",
+ moveInViewHolders.size() > lastPos - 51);
+ // run a scroll pass, the scroll pass should not remove the animating views even they are
+ // outside visible areas.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollBy(-3, 0);
+ }
+ });
+ waitOneUiCycle();
+ for (int i = 0; i < moveInViewHolders.size(); i++) {
+ assertSame(mGridView, moveInViewHolders.get(i).getParent());
+ }
+ assertSame(mGridView, moveViewHolder.itemView.getParent());
+ assertFalse(moveViewHolder.isRecyclable());
+ waitForItemAnimation();
+ assertNull(moveViewHolder.itemView.getParent());
+ assertTrue(moveViewHolder.isRecyclable());
+ }
+
+ @Test
+ public void testMoveItemToTheLeft() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setAddDuration(2000);
+ mGridView.getItemAnimator().setMoveDuration(2000);
+ mGridView.setSelectedPosition(1500);
+ }
+ });
+ waitForScrollIdle();
+ RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(1499);
+
+ int firstPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(0));
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.moveItem(1499, 1, true);
+ }
+ });
+ final ArrayList<View> moveInViewHolders = new ArrayList();
+ waitForItemAnimationStart();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
+ View v = mGridView.getLayoutManager().getChildAt(i);
+ if (mGridView.getChildAdapterPosition(v) <= 1499) {
+ moveInViewHolders.add(v);
+ }
+ }
+ }
+ });
+ waitOneUiCycle();
+ assertTrue("prelayout should layout extra items to slide in ",
+ moveInViewHolders.size() > 1499 - firstPos);
+ // run a scroll pass, the scroll pass should not remove the animating views even they are
+ // outside visible areas.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollBy(3, 0);
+ }
+ });
+ waitOneUiCycle();
+ for (int i = 0; i < moveInViewHolders.size(); i++) {
+ assertSame(mGridView, moveInViewHolders.get(i).getParent());
+ }
+ assertSame(mGridView, moveViewHolder.itemView.getParent());
+ assertFalse(moveViewHolder.isRecyclable());
+ waitForItemAnimation();
+ assertNull(moveViewHolder.itemView.getParent());
+ assertTrue(moveViewHolder.isRecyclable());
+ }
+
+ @Test
+ public void testContinuousSwapForward() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ for (int i = 150; i < 199; i++) {
+ final int swapIndex = i;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(swapIndex, swapIndex + 1);
+ }
+ });
+ Thread.sleep(10);
+ }
+ waitForItemAnimation();
+ assertEquals(199, mGridView.getSelectedPosition());
+ // check if ItemAnimation finishes at aligned positions:
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(199).getLeft();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(199).getLeft());
+ }
+
+ @Test
+ public void testContinuousSwapBackward() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ for (int i = 50; i > 0; i--) {
+ final int swapIndex = i;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(swapIndex, swapIndex - 1);
+ }
+ });
+ Thread.sleep(10);
+ }
+ waitForItemAnimation();
+ assertEquals(0, mGridView.getSelectedPosition());
+ // check if ItemAnimation finishes at aligned positions:
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(0).getLeft();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(0).getLeft());
+ }
+
+ @Test
+ public void testScrollAndStuck() throws Throwable {
+ // see b/67370222 fastRelayout() may be stuck.
+ final int numItems = 19;
+ final int[] itemsLength = new int[numItems];
+ for (int i = 0; i < numItems; i++) {
+ itemsLength[i] = 288;
+ }
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, itemsLength);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ // set left right padding to 112, space between items to be 16.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ViewGroup.LayoutParams lp = mGridView.getLayoutParams();
+ lp.width = 1920;
+ mGridView.setLayoutParams(lp);
+ mGridView.setPadding(112, mGridView.getPaddingTop(), 112,
+ mGridView.getPaddingBottom());
+ mGridView.setItemSpacing(16);
+ }
+ });
+ waitOneUiCycle();
+
+ int scrollPos = 0;
+ while (true) {
+ final View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ final int pos = mGridView.getChildViewHolder(view).getAdapterPosition();
+ if (scrollPos != pos) {
+ scrollPos = pos;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.smoothScrollToPosition(pos);
+ }
+ });
+ }
+ // wait until we see 2nd from last:
+ if (pos >= 17) {
+ if (pos == 17) {
+ // great we can test fastRelayout() bug.
+ Thread.sleep(50);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ view.requestLayout();
+ }
+ });
+ }
+ break;
+ }
+ Thread.sleep(16);
+ }
+ waitForScrollIdle();
+ }
+
+ @Test
+ public void testSwapAfterScroll() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setMoveDuration(1000);
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(151);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // we want to swap and select new target which is at 150 before swap
+ mGridView.setSelectedPositionSmooth(150);
+ mActivity.swap(150, 151);
+ }
+ });
+ waitForItemAnimation();
+ waitForScrollIdle();
+ assertEquals(151, mGridView.getSelectedPosition());
+ // check if ItemAnimation finishes at aligned positions:
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(151).getLeft();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(151).getLeft());
+ }
+
+ @Test
+ public void testItemMovedHorizontal() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(150, 152);
+ }
+ });
+ mActivityTestRule.runOnUiThread(mVerifyLayout);
+
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testItemMovedHorizontalRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_rtl);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[] {40, 40, 40});
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.moveItem(0, 1, true);
+ }
+ });
+ assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.findViewHolderForAdapterPosition(0).itemView.getRight());
+ }
+
+ @Test
+ public void testScrollSecondaryCannotScroll() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+ final int topPadding = 2;
+ final int bottomPadding = 2;
+ final int height = mGridView.getHeight();
+ final int spacing = 2;
+ final int rowHeight = (height - topPadding - bottomPadding) / 4 - spacing;
+ final HorizontalGridView horizontalGridView = (HorizontalGridView) mGridView;
+
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ horizontalGridView.setPadding(0, topPadding, 0, bottomPadding);
+ horizontalGridView.setItemSpacing(spacing);
+ horizontalGridView.setNumRows(mNumRows);
+ horizontalGridView.setRowHeight(rowHeight);
+ }
+ });
+ waitForLayout();
+ // navigate vertically in first column, first row should always be aligned to top padding
+ for (int i = 0; i < 3; i++) {
+ setSelectedPosition(i);
+ assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView
+ .getTop());
+ }
+ // navigate vertically in 100th column, first row should always be aligned to top padding
+ for (int i = 300; i < 301; i++) {
+ setSelectedPosition(i);
+ assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(300).itemView
+ .getTop());
+ }
+ }
+
+ @Test
+ public void testScrollSecondaryNeedScroll() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ // test a lot of rows so we have to scroll vertically to reach
+ mNumRows = 9;
+ final int topPadding = 2;
+ final int bottomPadding = 2;
+ final int height = mGridView.getHeight();
+ final int spacing = 2;
+ final int rowHeight = (height - topPadding - bottomPadding) / 4 - spacing;
+ final HorizontalGridView horizontalGridView = (HorizontalGridView) mGridView;
+
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ horizontalGridView.setPadding(0, topPadding, 0, bottomPadding);
+ horizontalGridView.setItemSpacing(spacing);
+ horizontalGridView.setNumRows(mNumRows);
+ horizontalGridView.setRowHeight(rowHeight);
+ }
+ });
+ waitForLayout();
+ View view;
+ // first row should be aligned to top padding
+ setSelectedPosition(0);
+ assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView.getTop());
+ // middle row should be aligned to keyline (1/2 of screen height)
+ setSelectedPosition(4);
+ view = mGridView.findViewHolderForAdapterPosition(4).itemView;
+ assertEquals(height / 2, (view.getTop() + view.getBottom()) / 2);
+ // last row should be aligned to bottom padding.
+ setSelectedPosition(8);
+ view = mGridView.findViewHolderForAdapterPosition(8).itemView;
+ assertEquals(height, view.getTop() + rowHeight + bottomPadding);
+ setSelectedPositionSmooth(4);
+ waitForScrollIdle();
+ // middle row should be aligned to keyline (1/2 of screen height)
+ setSelectedPosition(4);
+ view = mGridView.findViewHolderForAdapterPosition(4).itemView;
+ assertEquals(height / 2, (view.getTop() + view.getBottom()) / 2);
+ // first row should be aligned to top padding
+ setSelectedPositionSmooth(0);
+ waitForScrollIdle();
+ assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView.getTop());
+ }
+
+ @Test
+ public void testItemMovedVertical() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ mGridView.setSelectedPositionSmooth(150);
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(150, 152);
+ }
+ });
+ mActivityTestRule.runOnUiThread(mVerifyLayout);
+
+ scrollToEnd(mVerifyLayout);
+ scrollToBegin(mVerifyLayout);
+
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testAddLastItemHorizontal() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(49);
+ }
+ }
+ );
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{150});
+ }
+ });
+
+ // assert new added item aligned to right edge
+ assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getLayoutManager().findViewByPosition(50).getRight());
+ }
+
+ @Test
+ public void testAddMultipleLastItemsHorizontal() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_BOTH_EDGE);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setSelectedPositionSmooth(49);
+ }
+ }
+ );
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150});
+ }
+ });
+
+ // The focused item will be at center of window
+ View view = mGridView.getLayoutManager().findViewByPosition(49);
+ assertEquals(mGridView.getWidth() / 2, (view.getLeft() + view.getRight()) / 2);
+ }
+
+ @Test
+ public void testItemAddRemoveHorizontal() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ scrollToEnd(mVerifyLayout);
+ int[] endEdges = getEndEdges();
+
+ mGridView.setSelectedPositionSmooth(150);
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mRemovedItems = mActivity.removeItems(151, 4);
+ }
+ });
+
+ scrollToEnd(mVerifyLayout);
+ mGridView.setSelectedPositionSmooth(150);
+ waitForScrollIdle(mVerifyLayout);
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(151, mRemovedItems);
+ }
+ });
+ scrollToEnd(mVerifyLayout);
+
+ // we should get same aligned end edges
+ int[] endEdges2 = getEndEdges();
+ verifyEdgesSame(endEdges, endEdges2);
+
+ scrollToBegin(mVerifyLayout);
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testSetSelectedPositionDetached() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int focusToIndex = 49;
+ final ViewGroup parent = (ViewGroup) mGridView.getParent();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ parent.removeView(mGridView);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ parent.addView(mGridView);
+ mGridView.requestFocus();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(mGridView.getSelectedPosition(), focusToIndex);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(focusToIndex).hasFocus());
+
+ final int focusToIndex2 = 0;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ parent.removeView(mGridView);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPosition(focusToIndex2);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ parent.addView(mGridView);
+ mGridView.requestFocus();
+ }
+ });
+ assertEquals(mGridView.getSelectedPosition(), focusToIndex2);
+ waitForScrollIdle();
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(focusToIndex2).hasFocus());
+ }
+
+ @Test
+ public void testBug22209986() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int focusToIndex = mGridView.getChildCount() - 1;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ waitForScrollIdle();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex + 1);
+ }
+ });
+ // let the scroll running for a while and requestLayout during scroll
+ Thread.sleep(80);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertEquals(mGridView.getScrollState(), BaseGridView.SCROLL_STATE_SETTLING);
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
+ }
+
+ void testScrollAndRemove(int[] itemsLength, int numItems) throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ if (itemsLength != null) {
+ intent.putExtra(GridActivity.EXTRA_ITEMS, itemsLength);
+ } else {
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ }
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int focusToIndex = mGridView.getChildCount() - 1;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusToIndex, 1);
+ }
+ });
+
+ waitOneUiCycle();
+ waitForScrollIdle();
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft(), DELTA);
+ }
+
+ @Test
+ public void testScrollAndRemove() throws Throwable {
+ // test random lengths for 50 items
+ testScrollAndRemove(null, 50);
+ }
+
+ /**
+ * This test verifies if scroll limits are ignored when onLayoutChildren compensate remaining
+ * scroll distance. b/64931938
+ * In the test, second child is long, other children are short.
+ * Test scrolls to the long child, and when scrolling, remove the long child. We made it long
+ * to have enough remaining scroll distance when the layout pass kicks in.
+ * The onLayoutChildren() would compensate the remaining scroll distance, moving all items
+ * toward right, which will make the first item's left edge bigger than left padding,
+ * which would violate the "scroll limit of left" in a regular scroll case, but
+ * in layout pass, we still honor that scroll request, ignoring the scroll limit.
+ */
+ @Test
+ public void testScrollAndRemoveSample1() throws Throwable {
+ DisplayMetrics dm = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getResources().getDisplayMetrics();
+ // screen width for long item and 4DP for other items
+ int longItemLength = dm.widthPixels;
+ int shortItemLength = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, dm);
+ int[] items = new int[1000];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = shortItemLength;
+ }
+ items[1] = longItemLength;
+ testScrollAndRemove(items, 0);
+ }
+
+ @Test
+ public void testScrollAndInsert() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ int[] items = new int[1000];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300 + (int)(Math.random() * 100);
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ final int focusToIndex = mGridView.getChildAdapterPosition(view);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ int[] newItems = new int[]{300, 300, 300};
+ mActivity.addItems(0, newItems);
+ }
+ });
+ waitForScrollIdle();
+ int topEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(topEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop());
+ }
+
+ @Test
+ public void testScrollAndInsertBeforeVisibleItem() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ int[] items = new int[1000];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300 + (int)(Math.random() * 100);
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ final int focusToIndex = mGridView.getChildAdapterPosition(view);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ int[] newItems = new int[]{300, 300, 300};
+ mActivity.addItems(focusToIndex, newItems);
+ }
+ });
+ }
+
+ @Test
+ public void testSmoothScrollAndRemove() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int focusToIndex = 200;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusToIndex, 1);
+ }
+ });
+
+ assertTrue("removing the index of not attached child should not affect smooth scroller",
+ mGridView.getLayoutManager().isSmoothScrolling());
+ waitForScrollIdle();
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
+ }
+
+ @Test
+ public void testSmoothScrollAndRemove2() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int focusToIndex = 200;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusToIndex);
+ }
+ });
+
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int removeIndex = mGridView.getChildViewHolder(
+ mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
+ mActivity.removeItems(removeIndex, 1);
+ }
+ });
+ waitForLayout();
+
+ assertTrue("removing the index of attached child should not kill smooth scroller",
+ mGridView.getLayoutManager().isSmoothScrolling());
+ waitForItemAnimation();
+ waitForScrollIdle();
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
+ }
+
+ @Test
+ public void testPendingSmoothScrollAndRemove() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 630 + (int)(Math.random() * 100);
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(mGridView.getChildAt(0).hasFocus());
+
+ // Pressing lots of key to make sure smooth scroller is running
+ mGridView.mLayoutManager.mMaxPendingMoves = 100;
+ for (int i = 0; i < 100; i++) {
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+
+ assertTrue(mGridView.getLayoutManager().isSmoothScrolling());
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int removeIndex = mGridView.getChildViewHolder(
+ mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
+ mActivity.removeItems(removeIndex, 1);
+ }
+ });
+ waitForLayout();
+
+ assertTrue("removing the index of attached child should not kill smooth scroller",
+ mGridView.getLayoutManager().isSmoothScrolling());
+
+ waitForItemAnimation();
+ waitForScrollIdle();
+ int focusIndex = mGridView.getSelectedPosition();
+ int topEdge = mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(topEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop());
+ }
+
+ @Test
+ public void testFocusToFirstItem() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mRemovedItems = mActivity.removeItems(0, 200);
+ }
+ });
+
+ humanDelay(500);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(0, mRemovedItems);
+ }
+ });
+
+ humanDelay(500);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(0).hasFocus());
+
+ changeArraySize(0);
+
+ changeArraySize(200);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(0).hasFocus());
+ }
+
+ @Test
+ public void testNonFocusableHorizontal() throws Throwable {
+ final int numItems = 200;
+ final int startPos = 45;
+ final int skips = 20;
+ final int numColumns = 3;
+ final int endPos = startPos + numColumns * (skips + 1);
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ for (int i = startPos + mNumRows, j = 0; j < skips; i += mNumRows, j++) {
+ focusable[i] = false;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(startPos);
+ waitForScrollIdle(mVerifyLayout);
+
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ } else {
+ sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ }
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(endPos, mGridView.getSelectedPosition());
+
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ } else {
+ sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ }
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(startPos, mGridView.getSelectedPosition());
+
+ }
+
+ @Test
+ public void testNoInitialFocusable() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ final int numItems = 100;
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+ boolean[] focusable = new boolean[numItems];
+ final int firstFocusableIndex = 10;
+ for (int i = 0; i < firstFocusableIndex; i++) {
+ focusable[i] = false;
+ }
+ for (int i = firstFocusableIndex; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+ assertTrue(mGridView.isFocused());
+
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ } else {
+ sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ }
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(firstFocusableIndex, mGridView.getSelectedPosition());
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(firstFocusableIndex).hasFocus());
+ }
+
+ @Test
+ public void testFocusOutOfEmptyListView() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ final int numItems = 100;
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+ initActivity(intent);
+
+ final View horizontalGridView = new HorizontalGridViewEx(mGridView.getContext());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ horizontalGridView.setFocusable(true);
+ horizontalGridView.setFocusableInTouchMode(true);
+ horizontalGridView.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
+ ((ViewGroup) mGridView.getParent()).addView(horizontalGridView, 0);
+ horizontalGridView.requestFocus();
+ }
+ });
+
+ assertTrue(horizontalGridView.isFocused());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+
+ assertTrue(mGridView.hasFocus());
+ }
+
+ @Test
+ public void testTransferFocusToChildWhenGainFocus() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ final int numItems = 100;
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+ boolean[] focusable = new boolean[numItems];
+ final int firstFocusableIndex = 1;
+ for (int i = 0; i < firstFocusableIndex; i++) {
+ focusable[i] = false;
+ }
+ for (int i = firstFocusableIndex; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ assertEquals(firstFocusableIndex, mGridView.getSelectedPosition());
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(firstFocusableIndex).hasFocus());
+ }
+
+ @Test
+ public void testFocusFromSecondChild() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ final int numItems = 100;
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = false;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ // switching Adapter to cause a full rebind, test if it will focus to second item.
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.mNumItems = numItems;
+ mActivity.mItemFocusables[1] = true;
+ mActivity.rebindToNewAdapter();
+ }
+ });
+ assertTrue(mGridView.findViewHolderForAdapterPosition(1).itemView.hasFocus());
+ }
+
+ @Test
+ public void removeFocusableItemAndFocusableRecyclerViewGetsFocus() throws Throwable {
+ final int numItems = 100;
+ final int numColumns = 3;
+ final int focusableIndex = 2;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = false;
+ }
+ focusable[focusableIndex] = true;
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusableIndex);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(focusableIndex, mGridView.getSelectedPosition());
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusableIndex, 1);
+ }
+ });
+ assertTrue(dumpGridView(mGridView), mGridView.isFocused());
+ }
+
+ @Test
+ public void removeFocusableItemAndUnFocusableRecyclerViewLosesFocus() throws Throwable {
+ final int numItems = 100;
+ final int numColumns = 3;
+ final int focusableIndex = 2;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = false;
+ }
+ focusable[focusableIndex] = true;
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setFocusableInTouchMode(false);
+ mGridView.setFocusable(false);
+ mGridView.setSelectedPositionSmooth(focusableIndex);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(focusableIndex, mGridView.getSelectedPosition());
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusableIndex, 1);
+ }
+ });
+ assertFalse(dumpGridView(mGridView), mGridView.hasFocus());
+ }
+
+ @Test
+ public void testNonFocusableVertical() throws Throwable {
+ final int numItems = 200;
+ final int startPos = 44;
+ final int skips = 20;
+ final int numColumns = 3;
+ final int endPos = startPos + numColumns * (skips + 1);
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ for (int i = startPos + mNumRows, j = 0; j < skips; i += mNumRows, j++) {
+ focusable[i] = false;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(startPos);
+ waitForScrollIdle(mVerifyLayout);
+
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(endPos, mGridView.getSelectedPosition());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_UP);
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(startPos, mGridView.getSelectedPosition());
+
+ }
+
+ @Test
+ public void testLtrFocusOutStartDisabled() throws Throwable {
+ final int numItems = 200;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid_ltr);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestFocus();
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(mGridView.hasFocus());
+ }
+
+ @Test
+ public void testRtlFocusOutStartDisabled() throws Throwable {
+ final int numItems = 200;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid_rtl);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestFocus();
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(mGridView.hasFocus());
+ }
+
+ @Test
+ public void testTransferFocusable() throws Throwable {
+ final int numItems = 200;
+ final int numColumns = 3;
+ final int startPos = 1;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ for (int i = 0; i < startPos; i++) {
+ focusable[i] = false;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ changeArraySize(0);
+ assertTrue(mGridView.isFocused());
+
+ changeArraySize(numItems);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
+ }
+
+ @Test
+ public void testTransferFocusable2() throws Throwable {
+ final int numItems = 200;
+ final int numColumns = 3;
+ final int startPos = 3; // make sure view at startPos is in visible area.
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = true;
+ }
+ for (int i = 0; i < startPos; i++) {
+ focusable[i] = false;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
+
+ changeArraySize(0);
+ assertTrue(mGridView.isFocused());
+
+ changeArraySize(numItems);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
+ }
+
+ @Test
+ public void testNonFocusableLoseInFastLayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ int[] items = new int[300];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 480;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+ int pressDown = 15;
+
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+
+ for (int i = 0; i < pressDown; i++) {
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ waitForScrollIdle(mVerifyLayout);
+ assertFalse(mGridView.isFocused());
+
+ }
+
+ @Test
+ public void testFocusableViewAvailable() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE,
+ new boolean[]{false, false, true, false, false});
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // RecyclerView does not respect focusable and focusableInTouchMode flag, so
+ // set flags in code.
+ mGridView.setFocusableInTouchMode(false);
+ mGridView.setFocusable(false);
+ }
+ });
+
+ assertFalse(mGridView.isFocused());
+
+ final boolean[] scrolled = new boolean[]{false};
+ mGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy){
+ if (dy > 0) {
+ scrolled[0] = true;
+ }
+ }
+ });
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(0, new int[]{200, 300, 500, 500, 200});
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ assertFalse("GridView should not be scrolled", scrolled[0]);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(2).hasFocus());
+
+ }
+
+ @Test
+ public void testSetSelectionWithDelta() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(3);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int top1 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
+
+ humanDelay(1000);
+
+ // scroll to position with delta
+ setSelectedPosition(3, 100);
+ int top2 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
+ assertEquals(top1 - 100, top2);
+
+ // scroll to same position without delta, it will be reset
+ setSelectedPosition(3, 0);
+ int top3 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
+ assertEquals(top1, top3);
+
+ // scroll invisible item after last visible item
+ final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getLastVisibleIndex();
+ setSelectedPosition(lastVisiblePos + 1, 100);
+ int top4 = mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1).getTop();
+ assertEquals(top1 - 100, top4);
+
+ // scroll invisible item before first visible item
+ final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getFirstVisibleIndex();
+ setSelectedPosition(firstVisiblePos - 1, 100);
+ int top5 = mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1).getTop();
+ assertEquals(top1 - 100, top5);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(50, 100);
+ int top6 = mGridView.getLayoutManager().findViewByPosition(50).getTop();
+ assertEquals(top1 - 100, top6);
+
+ // scroll to invisible item that is far away.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(100);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int top7 = mGridView.getLayoutManager().findViewByPosition(100).getTop();
+ assertEquals(top1, top7);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(10, 50);
+ int top8 = mGridView.getLayoutManager().findViewByPosition(10).getTop();
+ assertEquals(top1 - 50, top8);
+ }
+
+ @Test
+ public void testSetSelectionWithDeltaInGrid() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(10);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int top1 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
+
+ humanDelay(500);
+
+ // scroll to position with delta
+ setSelectedPosition(20, 100);
+ int top2 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
+ assertEquals(top1 - 100, top2);
+
+ // scroll to same position without delta, it will be reset
+ setSelectedPosition(20, 0);
+ int top3 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
+ assertEquals(top1, top3);
+
+ // scroll invisible item after last visible item
+ final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getLastVisibleIndex();
+ setSelectedPosition(lastVisiblePos + 1, 100);
+ int top4 = getCenterY(mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1));
+ verifyMargin();
+ assertEquals(top1 - 100, top4);
+
+ // scroll invisible item before first visible item
+ final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getFirstVisibleIndex();
+ setSelectedPosition(firstVisiblePos - 1, 100);
+ int top5 = getCenterY(mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1));
+ assertEquals(top1 - 100, top5);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(100, 100);
+ int top6 = getCenterY(mGridView.getLayoutManager().findViewByPosition(100));
+ assertEquals(top1 - 100, top6);
+
+ // scroll to invisible item that is far away.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(200);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ Thread.sleep(500);
+ int top7 = getCenterY(mGridView.getLayoutManager().findViewByPosition(200));
+ assertEquals(top1, top7);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(10, 50);
+ int top8 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
+ assertEquals(top1 - 50, top8);
+ }
+
+
+ @Test
+ public void testSetSelectionWithDeltaInGrid1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
+ 193,176,153,141,203,184,232,139,177,206,222,136,132,237,172,137,
+ 188,172,163,213,158,219,209,147,133,229,170,197,138,215,188,205,
+ 223,192,225,170,195,127,229,229,210,195,134,142,160,139,130,222,
+ 150,163,180,176,157,137,234,169,159,167,182,150,224,231,202,236,
+ 123,140,181,223,120,185,183,221,123,210,134,158,166,208,149,128,
+ 192,214,212,198,133,140,158,133,229,173,226,141,180,128,127,218,
+ 192,235,183,213,216,150,143,193,125,141,219,210,195,195,192,191,
+ 212,236,157,189,160,220,147,158,220,199,233,231,201,180,168,141,
+ 156,204,191,183,190,153,123,210,238,151,139,221,223,200,175,191,
+ 132,184,197,204,236,157,230,151,195,219,212,143,172,149,219,184,
+ 164,211,132,187,172,142,174,146,127,147,206,238,188,129,199,226,
+ 132,220,210,159,235,153,208,182,196,123,180,159,131,135,175,226,
+ 127,134,237,211,133,225,132,124,160,226,224,200,173,137,217,169,
+ 182,183,176,185,122,168,195,159,172,129,126,129,166,136,149,220,
+ 178,191,192,238,180,208,234,154,222,206,239,228,129,140,203,125,
+ 214,175,125,169,196,132,234,138,192,142,234,190,215,232,239,122,
+ 188,158,128,221,159,237,207,157,232,138,132,214,122,199,121,191,
+ 199,209,126,164,175,187,173,186,194,224,191,196,146,208,213,210,
+ 164,176,202,213,123,157,179,138,217,129,186,166,237,211,157,130,
+ 137,132,171,232,216,239,180,151,137,132,190,133,218,155,171,227,
+ 193,147,197,164,120,218,193,154,170,196,138,222,161,235,143,154,
+ 192,178,228,195,178,133,203,178,173,206,178,212,136,157,169,124,
+ 172,121,128,223,238,125,217,187,184,156,169,215,231,124,210,174,
+ 146,226,185,134,223,228,183,182,136,133,199,146,180,233,226,225,
+ 174,233,145,235,216,170,192,171,132,132,134,223,233,148,154,162,
+ 192,179,197,203,139,197,174,187,135,132,180,136,192,195,124,221,
+ 120,189,233,233,146,225,234,163,215,143,132,198,156,205,151,190,
+ 204,239,221,229,123,138,134,217,219,136,218,215,167,139,195,125,
+ 202,225,178,226,145,208,130,194,228,197,157,215,124,147,174,123,
+ 237,140,172,181,161,151,229,216,199,199,179,213,146,122,222,162,
+ 139,173,165,150,160,217,207,137,165,175,129,158,134,133,178,199,
+ 215,213,122,197
+ });
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(10);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int top1 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
+
+ humanDelay(500);
+
+ // scroll to position with delta
+ setSelectedPosition(20, 100);
+ int top2 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
+ assertEquals(top1 - 100, top2);
+
+ // scroll to same position without delta, it will be reset
+ setSelectedPosition(20, 0);
+ int top3 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
+ assertEquals(top1, top3);
+
+ // scroll invisible item after last visible item
+ final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getLastVisibleIndex();
+ setSelectedPosition(lastVisiblePos + 1, 100);
+ int top4 = getCenterY(mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1));
+ verifyMargin();
+ assertEquals(top1 - 100, top4);
+
+ // scroll invisible item before first visible item
+ final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
+ .mGrid.getFirstVisibleIndex();
+ setSelectedPosition(firstVisiblePos - 1, 100);
+ int top5 = getCenterY(mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1));
+ assertEquals(top1 - 100, top5);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(100, 100);
+ int top6 = getCenterY(mGridView.getLayoutManager().findViewByPosition(100));
+ assertEquals(top1 - 100, top6);
+
+ // scroll to invisible item that is far away.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(200);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ Thread.sleep(500);
+ int top7 = getCenterY(mGridView.getLayoutManager().findViewByPosition(200));
+ assertEquals(top1, top7);
+
+ // scroll to invisible item that is far away.
+ setSelectedPosition(10, 50);
+ int top8 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
+ assertEquals(top1 - 50, top8);
+ }
+
+ @Test
+ public void testSmoothScrollSelectionEvents() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(30);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+ final ArrayList<Integer> selectedPositions = new ArrayList<Integer>();
+ mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ selectedPositions.add(position);
+ }
+ });
+
+ sendRepeatedKeys(10, KeyEvent.KEYCODE_DPAD_UP);
+ humanDelay(500);
+ waitForScrollIdle(mVerifyLayout);
+ // should only get childselected event for item 0 once
+ assertTrue(selectedPositions.size() > 0);
+ assertEquals(0, selectedPositions.get(selectedPositions.size() - 1).intValue());
+ for (int i = selectedPositions.size() - 2; i >= 0; i--) {
+ assertFalse(0 == selectedPositions.get(i).intValue());
+ }
+
+ }
+
+ @Test
+ public void testSmoothScrollSelectionEventsLinear() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(10);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+ final ArrayList<Integer> selectedPositions = new ArrayList<Integer>();
+ mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ selectedPositions.add(position);
+ }
+ });
+
+ sendRepeatedKeys(10, KeyEvent.KEYCODE_DPAD_UP);
+ humanDelay(500);
+ waitForScrollIdle(mVerifyLayout);
+ // should only get childselected event for item 0 once
+ assertTrue(selectedPositions.size() > 0);
+ assertEquals(0, selectedPositions.get(selectedPositions.size() - 1).intValue());
+ for (int i = selectedPositions.size() - 2; i >= 0; i--) {
+ assertFalse(0 == selectedPositions.get(i).intValue());
+ }
+
+ }
+
+ @Test
+ public void testScrollToNoneExisting() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(99);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ Thread.sleep(100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+ }
+
+ @Test
+ public void testSmoothscrollerInterrupted() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 680;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(mGridView.getChildAt(0).hasFocus());
+
+ // Pressing lots of key to make sure smooth scroller is running
+ for (int i = 0; i < 20; i++) {
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ while (mGridView.getLayoutManager().isSmoothScrolling()
+ || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
+ // Repeatedly pressing to make sure pending keys does not drop to zero.
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ }
+
+ @Test
+ public void testSmoothscrollerCancelled() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 680;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(mGridView.getChildAt(0).hasFocus());
+
+ int targetPosition = items.length - 1;
+ mGridView.setSelectedPositionSmooth(targetPosition);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.stopScroll();
+ }
+ });
+ waitForScrollIdle();
+ waitForItemAnimation();
+ assertEquals(mGridView.getSelectedPosition(), targetPosition);
+ assertSame(mGridView.getLayoutManager().findViewByPosition(targetPosition),
+ mGridView.findFocus());
+ }
+
+ @Test
+ public void testSetNumRowsAndAddItem() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+
+ mActivity.addItems(items.length, new int[]{300});
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ((VerticalGridView) mGridView).setNumColumns(2);
+ }
+ });
+ Thread.sleep(1000);
+ assertTrue(mGridView.getChildAt(2).getLeft() != mGridView.getChildAt(1).getLeft());
+ }
+
+
+ @Test
+ public void testRequestLayoutBugInLayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(1);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+
+ sendKey(KeyEvent.KEYCODE_DPAD_UP);
+ waitForScrollIdle(mVerifyLayout);
+
+ assertEquals("Line 2", ((TextView) mGridView.findFocus()).getText().toString());
+ }
+
+
+ @Test
+ public void testChangeLayoutInChild() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_wrap_content);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ verifyMargin();
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(1);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ verifyMargin();
+ }
+
+ @Test
+ public void testWrapContent() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid_wrap);
+ int[] items = new int[200];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.attachToNewAdapter(new int[0]);
+ }
+ });
+
+ }
+
+ @Test
+ public void testZeroFixedSecondarySize() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_measured_with_zero);
+ intent.putExtra(GridActivity.EXTRA_SECONDARY_SIZE_ZERO, true);
+ int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 0;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ }
+
+ @Test
+ public void testChildStates() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 200;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_ALL_CHILD);
+
+ final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
+
+ // 1 Save view states
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(0))
+ .getText()), 0, 1);
+ Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(1))
+ .getText()), 0, 1);
+ mGridView.saveHierarchyState(container);
+ }
+ });
+
+ // 2 Change view states
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(0))
+ .getText()), 1, 2);
+ Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(1))
+ .getText()), 1, 2);
+ }
+ });
+
+ // 3 Detached and re-attached, should still maintain state of (2)
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(1);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionStart(), 1);
+ assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionEnd(), 2);
+ assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionStart(), 1);
+ assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionEnd(), 2);
+
+ // 4 Recycled and rebound, should load state from (2)
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(20);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionStart(), 1);
+ assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionEnd(), 2);
+ assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionStart(), 1);
+ assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionEnd(), 2);
+ }
+
+
+ @Test
+ public void testNoDispatchSaveChildState() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 200;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_NO_CHILD);
+
+ final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
+
+ // 1. Set text selection, save view states should do nothing on child
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(i))
+ .getText()), 0, 1);
+ }
+ mGridView.saveHierarchyState(container);
+ }
+ });
+
+ // 2. clear the text selection
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ Selection.removeSelection((Spannable)(((TextView) mGridView.getChildAt(i))
+ .getText()));
+ }
+ }
+ });
+
+ // 3. Restore view states should be a no-op for child
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.restoreHierarchyState(container);
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ assertEquals(-1, ((TextView) mGridView.getChildAt(i)).getSelectionStart());
+ assertEquals(-1, ((TextView) mGridView.getChildAt(i)).getSelectionEnd());
+ }
+ }
+ });
+ }
+
+
+ static interface ViewTypeProvider {
+ public int getViewType(int position);
+ }
+
+ static interface ItemAlignmentFacetProvider {
+ public ItemAlignmentFacet getItemAlignmentFacet(int viewType);
+ }
+
+ static class TwoViewTypesProvider implements ViewTypeProvider {
+ static int VIEW_TYPE_FIRST = 1;
+ static int VIEW_TYPE_DEFAULT = 0;
+ @Override
+ public int getViewType(int position) {
+ if (position == 0) {
+ return VIEW_TYPE_FIRST;
+ } else {
+ return VIEW_TYPE_DEFAULT;
+ }
+ }
+ }
+
+ static class ChangeableViewTypesProvider implements ViewTypeProvider {
+ static SparseIntArray sViewTypes = new SparseIntArray();
+ @Override
+ public int getViewType(int position) {
+ return sViewTypes.get(position);
+ }
+ public static void clear() {
+ sViewTypes.clear();
+ }
+ public static void setViewType(int position, int type) {
+ sViewTypes.put(position, type);
+ }
+ }
+
+ static class PositionItemAlignmentFacetProviderForRelativeLayout1
+ implements ItemAlignmentFacetProvider {
+ ItemAlignmentFacet mMultipleFacet;
+
+ PositionItemAlignmentFacetProviderForRelativeLayout1() {
+ mMultipleFacet = new ItemAlignmentFacet();
+ ItemAlignmentFacet.ItemAlignmentDef[] defs =
+ new ItemAlignmentFacet.ItemAlignmentDef[2];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t1);
+ defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[1].setItemAlignmentViewId(R.id.t2);
+ defs[1].setItemAlignmentOffsetPercent(100);
+ defs[1].setItemAlignmentOffset(-10);
+ mMultipleFacet.setAlignmentDefs(defs);
+ }
+
+ @Override
+ public ItemAlignmentFacet getItemAlignmentFacet(int position) {
+ if (position == 0) {
+ return mMultipleFacet;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Test
+ public void testMultipleScrollPosition1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ TwoViewTypesProvider.class.getName());
+ // Set ItemAlignment for each ViewHolder and view type, ViewHolder should
+ // override the view type settings.
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
+ PositionItemAlignmentFacetProviderForRelativeLayout1.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS,
+ ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top",
+ mGridView.getPaddingTop(), mGridView.getChildAt(0).getTop());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(mVerifyLayout);
+
+ final View v = mGridView.getChildAt(0);
+ View t1 = v.findViewById(R.id.t1);
+ int t1align = (t1.getTop() + t1.getBottom()) / 2;
+ View t2 = v.findViewById(R.id.t2);
+ int t2align = t2.getBottom() - 10;
+ assertEquals("Expected alignment for 2nd textview",
+ mGridView.getPaddingTop() - (t2align - t1align),
+ v.getTop());
+ }
+
+ static class PositionItemAlignmentFacetProviderForRelativeLayout2 implements
+ ItemAlignmentFacetProvider {
+ ItemAlignmentFacet mMultipleFacet;
+
+ PositionItemAlignmentFacetProviderForRelativeLayout2() {
+ mMultipleFacet = new ItemAlignmentFacet();
+ ItemAlignmentFacet.ItemAlignmentDef[] defs = new ItemAlignmentFacet.ItemAlignmentDef[2];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t1);
+ defs[0].setItemAlignmentOffsetPercent(0);
+ defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[1].setItemAlignmentViewId(R.id.t2);
+ defs[1].setItemAlignmentOffsetPercent(ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ defs[1].setItemAlignmentOffset(-10);
+ mMultipleFacet.setAlignmentDefs(defs);
+ }
+
+ @Override
+ public ItemAlignmentFacet getItemAlignmentFacet(int position) {
+ if (position == 0) {
+ return mMultipleFacet;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Test
+ public void testMultipleScrollPosition2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ TwoViewTypesProvider.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
+ PositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(mVerifyLayout);
+
+ final View v = mGridView.getChildAt(0);
+ View t1 = v.findViewById(R.id.t1);
+ int t1align = t1.getTop();
+ View t2 = v.findViewById(R.id.t2);
+ int t2align = t2.getTop() - 10;
+ assertEquals("Expected alignment for 2nd textview",
+ mGridView.getPaddingTop() - (t2align - t1align), v.getTop());
+ }
+
+ static class ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2 implements
+ ItemAlignmentFacetProvider {
+ ItemAlignmentFacet mMultipleFacet;
+
+ ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2() {
+ mMultipleFacet = new ItemAlignmentFacet();
+ ItemAlignmentFacet.ItemAlignmentDef[] defs = new ItemAlignmentFacet.ItemAlignmentDef[2];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t1);
+ defs[0].setItemAlignmentOffsetPercent(0);
+ defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[1].setItemAlignmentViewId(R.id.t2);
+ defs[1].setItemAlignmentOffsetPercent(100);
+ defs[1].setItemAlignmentOffset(-10);
+ mMultipleFacet.setAlignmentDefs(defs);
+ }
+
+ @Override
+ public ItemAlignmentFacet getItemAlignmentFacet(int viewType) {
+ if (viewType == TwoViewTypesProvider.VIEW_TYPE_FIRST) {
+ return mMultipleFacet;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Test
+ public void testMultipleScrollPosition3() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ TwoViewTypesProvider.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS,
+ ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ waitForScrollIdle(mVerifyLayout);
+
+ final View v = mGridView.getChildAt(0);
+ View t1 = v.findViewById(R.id.t1);
+ int t1align = t1.getTop();
+ View t2 = v.findViewById(R.id.t2);
+ int t2align = t2.getBottom() - 10;
+ assertEquals("Expected alignment for 2nd textview",
+ mGridView.getPaddingTop() - (t2align - t1align), v.getTop());
+ }
+
+ @Test
+ public void testSelectionAndAddItemInOneCycle() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(0, new int[]{300, 300});
+ mGridView.setSelectedPosition(0);
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testSelectViewTaskSmoothWithAdapterChange() throws Throwable {
+ testSelectViewTaskWithAdapterChange(true /*smooth*/);
+ }
+
+ @Test
+ public void testSelectViewTaskWithAdapterChange() throws Throwable {
+ testSelectViewTaskWithAdapterChange(false /*smooth*/);
+ }
+
+ private void testSelectViewTaskWithAdapterChange(final boolean smooth) throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final View firstView = mGridView.getLayoutManager().findViewByPosition(0);
+ final View[] selectedViewByTask = new View[1];
+ final ViewHolderTask task = new ViewHolderTask() {
+ @Override
+ public void run(RecyclerView.ViewHolder viewHolder) {
+ selectedViewByTask[0] = viewHolder.itemView;
+ }
+ };
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 1);
+ if (smooth) {
+ mGridView.setSelectedPositionSmooth(0, task);
+ } else {
+ mGridView.setSelectedPosition(0, task);
+ }
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ assertNotNull(selectedViewByTask[0]);
+ assertNotSame(firstView, selectedViewByTask[0]);
+ assertSame(mGridView.getLayoutManager().findViewByPosition(0), selectedViewByTask[0]);
+ }
+
+ @Test
+ public void testNotifyItemTypeChangedSelectionEvent() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ ChangeableViewTypesProvider.class.getName());
+ ChangeableViewTypesProvider.clear();
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final ArrayList<Integer> selectedLog = new ArrayList<Integer>();
+ mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ selectedLog.add(position);
+ }
+ });
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ ChangeableViewTypesProvider.setViewType(0, 1);
+ mGridView.getAdapter().notifyItemChanged(0, 1);
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ assertEquals(selectedLog.size(), 1);
+ assertEquals((int) selectedLog.get(0), 0);
+ }
+
+ @Test
+ public void testNotifyItemChangedSelectionEvent() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ OnChildViewHolderSelectedListener listener =
+ Mockito.mock(OnChildViewHolderSelectedListener.class);
+ mGridView.setOnChildViewHolderSelectedListener(listener);
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyItemChanged(0, 1);
+ }
+ });
+ Mockito.verify(listener, times(1)).onChildViewHolderSelected(any(RecyclerView.class),
+ any(RecyclerView.ViewHolder.class), anyInt(), anyInt());
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testSelectionSmoothAndAddItemInOneCycle() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(0, new int[]{300, 300});
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testExtraLayoutSpace() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+
+ final int windowSize = mGridView.getHeight();
+ final int extraLayoutSize = windowSize;
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ // add extra layout space
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setExtraLayoutSpace(extraLayoutSize);
+ }
+ });
+ waitForLayout();
+ View v;
+ v = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ assertTrue(v.getTop() < windowSize + extraLayoutSize);
+ assertTrue(v.getBottom() >= windowSize + extraLayoutSize - mGridView.getVerticalMargin());
+
+ mGridView.setSelectedPositionSmooth(150);
+ waitForScrollIdle(mVerifyLayout);
+ v = mGridView.getChildAt(0);
+ assertTrue(v.getBottom() > - extraLayoutSize);
+ assertTrue(v.getTop() <= -extraLayoutSize + mGridView.getVerticalMargin());
+
+ // clear extra layout space
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setExtraLayoutSpace(0);
+ verifyMargin();
+ }
+ });
+ Thread.sleep(50);
+ v = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ assertTrue(v.getTop() < windowSize);
+ assertTrue(v.getBottom() >= windowSize - mGridView.getVerticalMargin());
+ }
+
+ @Test
+ public void testFocusFinder() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 3);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ // test focus from button to vertical grid view
+ final View button = mActivity.findViewById(R.id.button);
+ assertTrue(button.isFocused());
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ assertFalse(mGridView.isFocused());
+ assertTrue(mGridView.hasFocus());
+
+ // FocusFinder should find last focused(2nd) item on DPAD_DOWN
+ final View secondChild = mGridView.getChildAt(1);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ secondChild.requestFocus();
+ button.requestFocus();
+ }
+ });
+ assertTrue(button.isFocused());
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ assertTrue(secondChild.isFocused());
+
+ // Bug 26918143 Even VerticalGridView is not focusable, FocusFinder should find last focused
+ // (2nd) item on DPAD_DOWN.
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ button.requestFocus();
+ }
+ });
+ mGridView.setFocusable(false);
+ mGridView.setFocusableInTouchMode(false);
+ assertTrue(button.isFocused());
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
+ assertTrue(secondChild.isFocused());
+ }
+
+ @Test
+ public void testRestoreIndexAndAddItems() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 4);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ assertEquals(mGridView.getSelectedPosition(), 0);
+ final SparseArray<Parcelable> states = new SparseArray<>();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.saveHierarchyState(states);
+ mGridView.setAdapter(null);
+ }
+
+ });
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.restoreHierarchyState(states);
+ mActivity.attachToNewAdapter(new int[0]);
+ mActivity.addItems(0, new int[]{100, 100, 100, 100});
+ }
+
+ });
+ assertEquals(mGridView.getSelectedPosition(), 0);
+ }
+
+ @Test
+ public void testRestoreIndexAndAddItemsSelect1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 4);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPosition(1);
+ }
+
+ });
+ assertEquals(mGridView.getSelectedPosition(), 1);
+ final SparseArray<Parcelable> states = new SparseArray<>();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.saveHierarchyState(states);
+ mGridView.setAdapter(null);
+ }
+
+ });
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.restoreHierarchyState(states);
+ mActivity.attachToNewAdapter(new int[0]);
+ mActivity.addItems(0, new int[]{100, 100, 100, 100});
+ }
+
+ });
+ assertEquals(mGridView.getSelectedPosition(), 1);
+ }
+
+ @Test
+ public void testRestoreStateAfterAdapterChange() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{50, 50, 50, 50});
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPosition(1);
+ mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_ALL_CHILD);
+ }
+
+ });
+ assertEquals(mGridView.getSelectedPosition(), 1);
+ final SparseArray<Parcelable> states = new SparseArray<>();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Selection.setSelection((Spannable) (((TextView) mGridView.getChildAt(0))
+ .getText()), 1, 2);
+ Selection.setSelection((Spannable) (((TextView) mGridView.getChildAt(1))
+ .getText()), 0, 1);
+ mGridView.saveHierarchyState(states);
+ mGridView.setAdapter(null);
+ }
+
+ });
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.restoreHierarchyState(states);
+ mActivity.attachToNewAdapter(new int[]{50, 50, 50, 50});
+ }
+
+ });
+ assertEquals(mGridView.getSelectedPosition(), 1);
+ assertEquals(1, ((TextView) mGridView.getChildAt(0)).getSelectionStart());
+ assertEquals(2, ((TextView) mGridView.getChildAt(0)).getSelectionEnd());
+ assertEquals(0, ((TextView) mGridView.getChildAt(1)).getSelectionStart());
+ assertEquals(1, ((TextView) mGridView.getChildAt(1)).getSelectionEnd());
+ }
+
+ @Test
+ public void test27766012() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ // set remove animator two seconds
+ mGridView.getItemAnimator().setRemoveDuration(2000);
+ final View view = mGridView.getChildAt(1);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ view.requestFocus();
+ }
+ });
+ assertTrue(view.hasFocus());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 2);
+ }
+
+ });
+ // wait one second, removing second view is still attached to parent
+ Thread.sleep(1000);
+ assertSame(view.getParent(), mGridView);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // refocus to the removed item and do a focus search.
+ view.requestFocus();
+ view.focusSearch(View.FOCUS_UP);
+ }
+
+ });
+ }
+
+ @Test
+ public void testBug27258366() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ // move item1 500 pixels right, when focus is on item1, default focus finder will pick
+ // item0 and item2 for the best match of focusSearch(FOCUS_LEFT). The grid widget
+ // must override default addFocusables(), not to add item0 or item2.
+ mActivity.mAdapterListener = new GridActivity.AdapterListener() {
+ @Override
+ public void onBind(RecyclerView.ViewHolder vh, int position) {
+ if (position == 1) {
+ vh.itemView.setPaddingRelative(500, 0, 0, 0);
+ } else {
+ vh.itemView.setPaddingRelative(0, 0, 0, 0);
+ }
+ }
+ };
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyDataSetChanged();
+ }
+ });
+ Thread.sleep(100);
+
+ final ViewGroup secondChild = (ViewGroup) mGridView.getChildAt(1);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ secondChild.requestFocus();
+ }
+ });
+ sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(100);
+ final View button = mActivity.findViewById(R.id.button);
+ assertTrue(button.isFocused());
+ }
+
+ @Test
+ public void testUpdateHeightScrollHorizontal() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, true);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int childTop = mGridView.getChildAt(0).getTop();
+ // scroll to end, all children's top should not change.
+ scrollToEnd(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ assertEquals(childTop, mGridView.getChildAt(i).getTop());
+ }
+ }
+ });
+ // sanity check last child has focus with a larger height.
+ assertTrue(mGridView.getChildAt(0).getHeight()
+ < mGridView.getChildAt(mGridView.getChildCount() - 1).getHeight());
+ }
+
+ @Test
+ public void testUpdateWidthScrollHorizontal() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, true);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int childTop = mGridView.getChildAt(0).getTop();
+ // scroll to end, all children's top should not change.
+ scrollToEnd(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ assertEquals(childTop, mGridView.getChildAt(i).getTop());
+ }
+ }
+ });
+ // sanity check last child has focus with a larger width.
+ assertTrue(mGridView.getChildAt(0).getWidth()
+ < mGridView.getChildAt(mGridView.getChildCount() - 1).getWidth());
+ if (mGridView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ assertEquals(mGridView.getPaddingLeft(),
+ mGridView.getChildAt(mGridView.getChildCount() - 1).getLeft());
+ } else {
+ assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getChildAt(mGridView.getChildCount() - 1).getRight());
+ }
+ }
+
+ @Test
+ public void testUpdateWidthScrollHorizontalRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_rtl);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, true);
+ intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ final int childTop = mGridView.getChildAt(0).getTop();
+ // scroll to end, all children's top should not change.
+ scrollToEnd(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mGridView.getChildCount(); i++) {
+ assertEquals(childTop, mGridView.getChildAt(i).getTop());
+ }
+ }
+ });
+ // sanity check last child has focus with a larger width.
+ assertTrue(mGridView.getChildAt(0).getWidth()
+ < mGridView.getChildAt(mGridView.getChildCount() - 1).getWidth());
+ assertEquals(mGridView.getPaddingLeft(),
+ mGridView.getChildAt(mGridView.getChildCount() - 1).getLeft());
+ }
+
+ @Test
+ public void testAccessibility() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ assertTrue(0 == mGridView.getSelectedPosition());
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int selectedPosition1 = mGridView.getSelectedPosition();
+ assertTrue(0 < selectedPosition1);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ int selectedPosition2 = mGridView.getSelectedPosition();
+ assertTrue(selectedPosition2 < selectedPosition1);
+ }
+
+ @Test
+ public void testAccessibilityScrollForwardHalfVisible() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_bottom);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ int height = mGridView.getHeight() - mGridView.getPaddingTop()
+ - mGridView.getPaddingBottom();
+ final int childHeight = height - mGridView.getVerticalSpacing() - 100;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffset(100);
+ mGridView.setWindowAlignmentOffsetPercent(BaseGridView
+ .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGridView.setItemAlignmentOffset(0);
+ mGridView.setItemAlignmentOffsetPercent(BaseGridView
+ .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ }
+ });
+ mActivity.addItems(0, new int[]{childHeight, childHeight});
+ waitForItemAnimation();
+ setSelectedPosition(0);
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testAccessibilityScrollBackwardHalfVisible() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_top);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ int height = mGridView.getHeight() - mGridView.getPaddingTop()
+ - mGridView.getPaddingBottom();
+ final int childHeight = height - mGridView.getVerticalSpacing() - 100;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffset(100);
+ mGridView.setWindowAlignmentOffsetPercent(BaseGridView
+ .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGridView.setItemAlignmentOffset(0);
+ mGridView.setItemAlignmentOffsetPercent(BaseGridView
+ .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ }
+ });
+ mActivity.addItems(0, new int[]{childHeight, childHeight});
+ waitForItemAnimation();
+ setSelectedPosition(1);
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ void slideInAndWaitIdle() throws Throwable {
+ slideInAndWaitIdle(5000);
+ }
+
+ void slideInAndWaitIdle(long timeout) throws Throwable {
+ // animateIn() would reset position
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateIn();
+ }
+ });
+ PollingCheck.waitFor(timeout, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !mGridView.getLayoutManager().isSmoothScrolling()
+ && mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ }
+
+ @Test
+ public void testAnimateOutBlockScrollTo() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ // wait until sliding out.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ // scrollToPosition() should not affect slideOut status
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+ >= mGridView.getHeight());
+
+ slideInAndWaitIdle();
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+ }
+
+ @Test
+ public void testAnimateOutBlockSmoothScrolling() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[30];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ // wait until sliding out.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ // smoothScrollToPosition() should not affect slideOut status
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.smoothScrollToPosition(29);
+ }
+ });
+ PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+ >= mGridView.getHeight());
+
+ slideInAndWaitIdle();
+ View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ assertSame("Scrolled to last child",
+ mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
+ }
+
+ @Test
+ public void testAnimateOutBlockLongScrollTo() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[30];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ // wait until sliding out.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ // smoothScrollToPosition() should not affect slideOut status
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollToPosition(29);
+ }
+ });
+ PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+ >= mGridView.getHeight());
+
+ slideInAndWaitIdle();
+ View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
+ assertSame("Scrolled to last child",
+ mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
+ }
+
+ @Test
+ public void testAnimateOutBlockLayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ // wait until sliding out.
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ // change adapter should not affect slideOut status
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.changeItem(0, 200);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+ >= mGridView.getHeight());
+ assertEquals("onLayout suppressed during slide out", 300,
+ mGridView.getChildAt(0).getHeight());
+
+ slideInAndWaitIdle();
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+ // size of item should be updated immediately after slide in animation finishes:
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 200 == mGridView.getChildAt(0).getHeight();
+ }
+ });
+ }
+
+ @Test
+ public void testAnimateOutBlockFocusChange() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ mActivity.findViewById(R.id.button).requestFocus();
+ }
+ });
+ assertTrue(mActivity.findViewById(R.id.button).hasFocus());
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestFocus();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+ assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+ >= mGridView.getHeight());
+
+ slideInAndWaitIdle();
+ assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+ mGridView.getChildAt(0).getTop());
+ }
+
+ @Test
+ public void testHorizontalAnimateOutBlockScrollTo() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+ mGridView.getChildAt(0).getLeft());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getLeft() > mGridView.getPaddingLeft();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.scrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertTrue("First view is slided out", mGridView.getChildAt(0).getLeft()
+ > mGridView.getWidth());
+
+ slideInAndWaitIdle();
+ assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+ mGridView.getChildAt(0).getLeft());
+
+ }
+
+ @Test
+ public void testHorizontalAnimateOutRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_rtl);
+ int[] items = new int[100];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ assertEquals("First view is aligned with padding right",
+ mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getChildAt(0).getRight());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.animateOut();
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getChildAt(0).getRight()
+ < mGridView.getWidth() - mGridView.getPaddingRight();
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.smoothScrollToPosition(0);
+ }
+ });
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+ }
+ });
+
+ assertTrue("First view is slided out", mGridView.getChildAt(0).getRight() < 0);
+
+ slideInAndWaitIdle();
+ assertEquals("First view is aligned with padding right",
+ mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getChildAt(0).getRight());
+ }
+
+ @Test
+ public void testSmoothScrollerOutRange() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_linear_with_button_onleft);
+ intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
+ int[] items = new int[30];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 680;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ final View button = mActivity.findViewById(R.id.button);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ button.requestFocus();
+ }
+ });
+
+ mGridView.setSelectedPositionSmooth(0);
+ waitForScrollIdle(mVerifyLayout);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.setSelectedPositionSmooth(120);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertTrue(button.hasFocus());
+ int key;
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ }
+ sendKey(key);
+ // the GridView should has focus in its children
+ assertTrue(mGridView.hasFocus());
+ assertFalse(mGridView.isFocused());
+ assertEquals(29, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testRemoveLastItemWithStableId() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, true);
+ int[] items = new int[1];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 680;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setRemoveDuration(2000);
+ mActivity.removeItems(0, 1, false);
+ mGridView.getAdapter().notifyDataSetChanged();
+ }
+ });
+ Thread.sleep(500);
+ assertEquals(-1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testUpdateAndSelect1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyDataSetChanged();
+ mGridView.setSelectedPosition(1);
+ }
+ });
+ waitOneUiCycle();
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testUpdateAndSelect2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getAdapter().notifyDataSetChanged();
+ mGridView.setSelectedPosition(50);
+ }
+ });
+ waitOneUiCycle();
+ assertEquals(50, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testUpdateAndSelect3() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ int[] newItems = new int[100];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = mActivity.mItemLengths[0];
+ }
+ mActivity.addItems(0, newItems, false);
+ mGridView.getAdapter().notifyDataSetChanged();
+ mGridView.setSelectedPosition(50);
+ }
+ });
+ waitOneUiCycle();
+ assertEquals(50, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testFocusedPositonAfterRemoved1() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ final int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ setSelectedPosition(1);
+ assertEquals(1, mGridView.getSelectedPosition());
+
+ final int[] newItems = new int[3];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = 300;
+ }
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 2, true);
+ mActivity.addItems(0, newItems, true);
+ }
+ });
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testFocusedPositonAfterRemoved2() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ final int[] items = new int[2];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ setSelectedPosition(1);
+ assertEquals(1, mGridView.getSelectedPosition());
+
+ final int[] newItems = new int[3];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = 300;
+ }
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(1, 1, true);
+ mActivity.addItems(1, newItems, true);
+ }
+ });
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ static void assertNoCollectionItemInfo(AccessibilityNodeInfoCompat info) {
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat nodeInfoCompat =
+ info.getCollectionItemInfo();
+ if (nodeInfoCompat == null) {
+ return;
+ }
+ assertTrue(nodeInfoCompat.getRowIndex() < 0);
+ assertTrue(nodeInfoCompat.getColumnIndex() < 0);
+ }
+
+ /**
+ * This test would need talkback on.
+ */
+ @Test
+ public void testAccessibilityOfItemsBeingPushedOut() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ final int lastPos = mGridView.getChildAdapterPosition(
+ mGridView.getChildAt(mGridView.getChildCount() - 1));
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getLayoutManager().setItemPrefetchEnabled(false);
+ }
+ });
+ final int numItemsToPushOut = mNumRows;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // set longer enough so that accessibility service will initialize node
+ // within setImportantForAccessibility().
+ mGridView.getItemAnimator().setRemoveDuration(2000);
+ mGridView.getItemAnimator().setAddDuration(2000);
+ final int[] newItems = new int[numItemsToPushOut];
+ final int newItemValue = mActivity.mItemLengths[0];
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = newItemValue;
+ }
+ mActivity.addItems(lastPos - numItemsToPushOut + 1, newItems);
+ }
+ });
+ waitForItemAnimation();
+ }
+
+ /**
+ * This test simulates talkback by calling setImportanceForAccessibility at end of animation
+ */
+ @Test
+ public void simulatesAccessibilityOfItemsBeingPushedOut() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ final HashSet<View> moveAnimationViews = new HashSet();
+ mActivity.mImportantForAccessibilityListener =
+ new GridActivity.ImportantForAccessibilityListener() {
+ RecyclerView.LayoutManager mLM = mGridView.getLayoutManager();
+ @Override
+ public void onImportantForAccessibilityChanged(View view, int newValue) {
+ // simulates talkack, having setImportantForAccessibility to call
+ // onInitializeAccessibilityNodeInfoForItem() for the DISAPPEARING items.
+ if (moveAnimationViews.contains(view)) {
+ AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mLM.onInitializeAccessibilityNodeInfoForItem(
+ null, null, view, info);
+ }
+ }
+ };
+ final int lastPos = mGridView.getChildAdapterPosition(
+ mGridView.getChildAt(mGridView.getChildCount() - 1));
+ final int numItemsToPushOut = mNumRows;
+ for (int i = 0; i < numItemsToPushOut; i++) {
+ moveAnimationViews.add(
+ mGridView.getChildAt(mGridView.getChildCount() - 1 - i));
+ }
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setItemAnimator(new DefaultItemAnimator() {
+ @Override
+ public void onMoveFinished(RecyclerView.ViewHolder item) {
+ moveAnimationViews.remove(item.itemView);
+ }
+ });
+ mGridView.getLayoutManager().setItemPrefetchEnabled(false);
+ }
+ });
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int[] newItems = new int[numItemsToPushOut];
+ final int newItemValue = mActivity.mItemLengths[0] + 1;
+ for (int i = 0; i < newItems.length; i++) {
+ newItems[i] = newItemValue;
+ }
+ mActivity.addItems(lastPos - numItemsToPushOut + 1, newItems);
+ }
+ });
+ while (moveAnimationViews.size() != 0) {
+ Thread.sleep(100);
+ }
+ }
+
+ @Test
+ public void testAccessibilityNodeInfoOnRemovedFirstItem() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 6);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ final View lastView = mGridView.findViewHolderForAdapterPosition(0).itemView;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setRemoveDuration(20000);
+ mActivity.removeItems(0, 1);
+ }
+ });
+ waitForItemAnimationStart();
+ AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(lastView);
+ mGridView.getLayoutManager().onInitializeAccessibilityNodeInfoForItem(null, null,
+ lastView, info);
+ assertNoCollectionItemInfo(info);
+ }
+
+ @Test
+ public void testAccessibilityNodeInfoOnRemovedLastItem() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 6);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ initActivity(intent);
+
+ final View lastView = mGridView.findViewHolderForAdapterPosition(5).itemView;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setRemoveDuration(20000);
+ mActivity.removeItems(5, 1);
+ }
+ });
+ waitForItemAnimationStart();
+ AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(lastView);
+ mGridView.getLayoutManager().onInitializeAccessibilityNodeInfoForItem(null, null,
+ lastView, info);
+ assertNoCollectionItemInfo(info);
+ }
+
+ static class FiveViewTypesProvider implements ViewTypeProvider {
+
+ @Override
+ public int getViewType(int position) {
+ switch (position) {
+ case 0:
+ return 0;
+ case 1:
+ return 1;
+ case 2:
+ return 2;
+ case 3:
+ return 3;
+ case 4:
+ return 4;
+ }
+ return 199;
+ }
+ }
+
+ // Used by testItemAlignmentVertical() testItemAlignmentHorizontal()
+ static class ItemAlignmentWithPaddingFacetProvider implements
+ ItemAlignmentFacetProvider {
+ final ItemAlignmentFacet mFacet0;
+ final ItemAlignmentFacet mFacet1;
+ final ItemAlignmentFacet mFacet2;
+ final ItemAlignmentFacet mFacet3;
+ final ItemAlignmentFacet mFacet4;
+
+ ItemAlignmentWithPaddingFacetProvider() {
+ ItemAlignmentFacet.ItemAlignmentDef[] defs;
+ mFacet0 = new ItemAlignmentFacet();
+ defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t1);
+ defs[0].setItemAlignmentOffsetPercent(0);
+ defs[0].setItemAlignmentOffsetWithPadding(false);
+ mFacet0.setAlignmentDefs(defs);
+ mFacet1 = new ItemAlignmentFacet();
+ defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t1);
+ defs[0].setItemAlignmentOffsetPercent(0);
+ defs[0].setItemAlignmentOffsetWithPadding(true);
+ mFacet1.setAlignmentDefs(defs);
+ mFacet2 = new ItemAlignmentFacet();
+ defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t2);
+ defs[0].setItemAlignmentOffsetPercent(100);
+ defs[0].setItemAlignmentOffsetWithPadding(true);
+ mFacet2.setAlignmentDefs(defs);
+ mFacet3 = new ItemAlignmentFacet();
+ defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t2);
+ defs[0].setItemAlignmentOffsetPercent(50);
+ defs[0].setItemAlignmentOffsetWithPadding(true);
+ mFacet3.setAlignmentDefs(defs);
+ mFacet4 = new ItemAlignmentFacet();
+ defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
+ defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
+ defs[0].setItemAlignmentViewId(R.id.t2);
+ defs[0].setItemAlignmentOffsetPercent(50);
+ defs[0].setItemAlignmentOffsetWithPadding(false);
+ mFacet4.setAlignmentDefs(defs);
+ }
+
+ @Override
+ public ItemAlignmentFacet getItemAlignmentFacet(int viewType) {
+ switch (viewType) {
+ case 0:
+ return mFacet0;
+ case 1:
+ return mFacet1;
+ case 2:
+ return mFacet2;
+ case 3:
+ return mFacet3;
+ case 4:
+ return mFacet4;
+ }
+ return null;
+ }
+ }
+
+ @Test
+ public void testItemAlignmentVertical() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout2);
+ int[] items = new int[5];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ FiveViewTypesProvider.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
+ ItemAlignmentWithPaddingFacetProvider.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setWindowAlignmentOffset(0);
+ }
+ });
+ waitForLayout();
+
+ final float windowAlignCenter = mGridView.getHeight() / 2f;
+ Rect rect = new Rect();
+ View textView;
+
+ // test 1: does not include padding
+ textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
+ rect.set(0, 0, textView.getWidth(), textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.top, DELTA);
+
+ // test 2: including low padding
+ setSelectedPosition(1);
+ textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
+ assertTrue(textView.getPaddingTop() > 0);
+ rect.set(0, textView.getPaddingTop(), textView.getWidth(), textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.top, DELTA);
+
+ // test 3: including high padding
+ setSelectedPosition(2);
+ textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingBottom() > 0);
+ rect.set(0, 0, textView.getWidth(),
+ textView.getHeight() - textView.getPaddingBottom());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.bottom, DELTA);
+
+ // test 4: including padding will be ignored if offsetPercent is not 0 or 100
+ setSelectedPosition(3);
+ textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingTop() != textView.getPaddingBottom());
+ rect.set(0, 0, textView.getWidth(), textView.getHeight() / 2);
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.bottom, DELTA);
+
+ // test 5: does not include padding
+ setSelectedPosition(4);
+ textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingTop() != textView.getPaddingBottom());
+ rect.set(0, 0, textView.getWidth(), textView.getHeight() / 2);
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.bottom, DELTA);
+ }
+
+ @Test
+ public void testItemAlignmentHorizontal() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout3);
+ int[] items = new int[5];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ FiveViewTypesProvider.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
+ ItemAlignmentWithPaddingFacetProvider.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setWindowAlignmentOffset(0);
+ }
+ });
+ waitForLayout();
+
+ final float windowAlignCenter = mGridView.getWidth() / 2f;
+ Rect rect = new Rect();
+ View textView;
+
+ // test 1: does not include padding
+ textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
+ rect.set(0, 0, textView.getWidth(), textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.left, DELTA);
+
+ // test 2: including low padding
+ setSelectedPosition(1);
+ textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
+ assertTrue(textView.getPaddingLeft() > 0);
+ rect.set(textView.getPaddingLeft(), 0, textView.getWidth(), textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.left, DELTA);
+
+ // test 3: including high padding
+ setSelectedPosition(2);
+ textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingRight() > 0);
+ rect.set(0, 0, textView.getWidth() - textView.getPaddingRight(),
+ textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+
+ // test 4: including padding will be ignored if offsetPercent is not 0 or 100
+ setSelectedPosition(3);
+ textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
+ rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+
+ // test 5: does not include padding
+ setSelectedPosition(4);
+ textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
+ rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+ }
+
+ @Test
+ public void testItemAlignmentHorizontalRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout3);
+ int[] items = new int[5];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 300;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
+ FiveViewTypesProvider.class.getName());
+ intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
+ ItemAlignmentWithPaddingFacetProvider.class.getName());
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setWindowAlignmentOffset(0);
+ }
+ });
+ waitForLayout();
+
+ final float windowAlignCenter = mGridView.getWidth() / 2f;
+ Rect rect = new Rect();
+ View textView;
+
+ // test 1: does not include padding
+ textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
+ rect.set(0, 0, textView.getWidth(), textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+
+ // test 2: including low padding
+ setSelectedPosition(1);
+ textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
+ assertTrue(textView.getPaddingRight() > 0);
+ rect.set(0, 0, textView.getWidth() - textView.getPaddingRight(),
+ textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+
+ // test 3: including high padding
+ setSelectedPosition(2);
+ textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingLeft() > 0);
+ rect.set(textView.getPaddingLeft(), 0, textView.getWidth(),
+ textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.left, DELTA);
+
+ // test 4: including padding will be ignored if offsetPercent is not 0 or 100
+ setSelectedPosition(3);
+ textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
+ rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+
+ // test 5: does not include padding
+ setSelectedPosition(4);
+ textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
+ assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
+ rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
+ mGridView.offsetDescendantRectToMyCoords(textView, rect);
+ assertEquals(windowAlignCenter, rect.right, DELTA);
+ }
+
+ enum ItemLocation {
+ ITEM_AT_LOW,
+ ITEM_AT_KEY_LINE,
+ ITEM_AT_HIGH
+ };
+
+ static class ItemAt {
+ final int mScrollPosition;
+ final int mPosition;
+ final ItemLocation mLocation;
+
+ ItemAt(int scrollPosition, int position, ItemLocation loc) {
+ mScrollPosition = scrollPosition;
+ mPosition = position;
+ mLocation = loc;
+ }
+
+ ItemAt(int position, ItemLocation loc) {
+ mScrollPosition = position;
+ mPosition = position;
+ mLocation = loc;
+ }
+ }
+
+ /**
+ * When scroll to position, item at position is expected at given location.
+ */
+ static ItemAt itemAt(int position, ItemLocation location) {
+ return new ItemAt(position, location);
+ }
+
+ /**
+ * When scroll to scrollPosition, item at position is expected at given location.
+ */
+ static ItemAt itemAt(int scrollPosition, int position, ItemLocation location) {
+ return new ItemAt(scrollPosition, position, location);
+ }
+
+ void prepareKeyLineTest(int numItems) throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+ int[] items = new int[numItems];
+ for (int i = 0; i < items.length; i++) {
+ items[i] = 32;
+ }
+ intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ initActivity(intent);
+ }
+
+ public void testPreferKeyLine(final int windowAlignment,
+ final boolean preferKeyLineOverLow,
+ final boolean preferKeyLineOverHigh,
+ ItemLocation assertFirstItemLocation,
+ ItemLocation assertLastItemLocation) throws Throwable {
+ testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
+ itemAt(0, assertFirstItemLocation),
+ itemAt(mActivity.mNumItems - 1, assertLastItemLocation));
+ }
+
+ public void testPreferKeyLine(final int windowAlignment,
+ final boolean preferKeyLineOverLow,
+ final boolean preferKeyLineOverHigh,
+ ItemLocation assertFirstItemLocation,
+ ItemAt assertLastItemLocation) throws Throwable {
+ testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
+ itemAt(0, assertFirstItemLocation),
+ assertLastItemLocation);
+ }
+
+ public void testPreferKeyLine(final int windowAlignment,
+ final boolean preferKeyLineOverLow,
+ final boolean preferKeyLineOverHigh,
+ ItemAt assertFirstItemLocation,
+ ItemLocation assertLastItemLocation) throws Throwable {
+ testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
+ assertFirstItemLocation,
+ itemAt(mActivity.mNumItems - 1, assertLastItemLocation));
+ }
+
+ public void testPreferKeyLine(final int windowAlignment,
+ final boolean preferKeyLineOverLow,
+ final boolean preferKeyLineOverHigh,
+ ItemAt assertFirstItemLocation,
+ ItemAt assertLastItemLocation) throws Throwable {
+ TestPreferKeyLineOptions options = new TestPreferKeyLineOptions();
+ options.mAssertItemLocations = new ItemAt[] {assertFirstItemLocation,
+ assertLastItemLocation};
+ options.mPreferKeyLineOverLow = preferKeyLineOverLow;
+ options.mPreferKeyLineOverHigh = preferKeyLineOverHigh;
+ options.mWindowAlignment = windowAlignment;
+
+ options.mRtl = false;
+ testPreferKeyLine(options);
+
+ options.mRtl = true;
+ testPreferKeyLine(options);
+ }
+
+ static class TestPreferKeyLineOptions {
+ int mWindowAlignment;
+ boolean mPreferKeyLineOverLow;
+ boolean mPreferKeyLineOverHigh;
+ ItemAt[] mAssertItemLocations;
+ boolean mRtl;
+ }
+
+ public void testPreferKeyLine(final TestPreferKeyLineOptions options) throws Throwable {
+ startWaitLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (options.mRtl) {
+ mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
+ } else {
+ mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ mGridView.setWindowAlignment(options.mWindowAlignment);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setWindowAlignmentOffset(0);
+ mGridView.setWindowAlignmentPreferKeyLineOverLowEdge(options.mPreferKeyLineOverLow);
+ mGridView.setWindowAlignmentPreferKeyLineOverHighEdge(
+ options.mPreferKeyLineOverHigh);
+ }
+ });
+ waitForLayout();
+
+ final int paddingStart = mGridView.getPaddingStart();
+ final int paddingEnd = mGridView.getPaddingEnd();
+ final int windowAlignCenter = mGridView.getWidth() / 2;
+
+ for (int i = 0; i < options.mAssertItemLocations.length; i++) {
+ ItemAt assertItemLocation = options.mAssertItemLocations[i];
+ setSelectedPosition(assertItemLocation.mScrollPosition);
+ View view = mGridView.findViewHolderForAdapterPosition(assertItemLocation.mPosition)
+ .itemView;
+ switch (assertItemLocation.mLocation) {
+ case ITEM_AT_LOW:
+ if (options.mRtl) {
+ assertEquals(mGridView.getWidth() - paddingStart, view.getRight());
+ } else {
+ assertEquals(paddingStart, view.getLeft());
+ }
+ break;
+ case ITEM_AT_HIGH:
+ if (options.mRtl) {
+ assertEquals(paddingEnd, view.getLeft());
+ } else {
+ assertEquals(mGridView.getWidth() - paddingEnd, view.getRight());
+ }
+ break;
+ case ITEM_AT_KEY_LINE:
+ assertEquals(windowAlignCenter, (view.getLeft() + view.getRight()) / 2, DELTA);
+ break;
+ }
+ }
+ }
+
+ @Test
+ public void testPreferKeyLine1() throws Throwable {
+ prepareKeyLineTest(1);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
+ ItemLocation.ITEM_AT_HIGH, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
+ ItemLocation.ITEM_AT_HIGH, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ }
+
+ @Test
+ public void testPreferKeyLine2() throws Throwable {
+ prepareKeyLineTest(2);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
+ itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
+ itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
+ itemAt(0, 1, ItemLocation.ITEM_AT_HIGH),
+ itemAt(1, 1, ItemLocation.ITEM_AT_HIGH));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
+ itemAt(0, 0, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 0, ItemLocation.ITEM_AT_KEY_LINE));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
+ itemAt(0, 1, ItemLocation.ITEM_AT_HIGH),
+ itemAt(1, 1, ItemLocation.ITEM_AT_HIGH));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
+ itemAt(0, 0, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 0, ItemLocation.ITEM_AT_KEY_LINE));
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
+ itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
+ itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
+ itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
+ }
+
+ @Test
+ public void testPreferKeyLine10000() throws Throwable {
+ prepareKeyLineTest(10000);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
+ ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
+
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
+ testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
+ ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java b/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
rename to leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java b/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java b/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java b/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
rename to leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
diff --git a/v17/leanback/tests/res/drawable/ic_action_a.png b/leanback/tests/res/drawable/ic_action_a.png
similarity index 100%
rename from v17/leanback/tests/res/drawable/ic_action_a.png
rename to leanback/tests/res/drawable/ic_action_a.png
Binary files differ
diff --git a/v17/leanback/tests/res/drawable/spiderman.jpg b/leanback/tests/res/drawable/spiderman.jpg
similarity index 100%
rename from v17/leanback/tests/res/drawable/spiderman.jpg
rename to leanback/tests/res/drawable/spiderman.jpg
Binary files differ
diff --git a/v17/leanback/tests/res/layout/browse.xml b/leanback/tests/res/layout/browse.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/browse.xml
rename to leanback/tests/res/layout/browse.xml
diff --git a/v17/leanback/tests/res/layout/datepicker_alone.xml b/leanback/tests/res/layout/datepicker_alone.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/datepicker_alone.xml
rename to leanback/tests/res/layout/datepicker_alone.xml
diff --git a/v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml b/leanback/tests/res/layout/datepicker_with_other_widgets.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml
rename to leanback/tests/res/layout/datepicker_with_other_widgets.xml
diff --git a/v17/leanback/tests/res/layout/details.xml b/leanback/tests/res/layout/details.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/details.xml
rename to leanback/tests/res/layout/details.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid.xml b/leanback/tests/res/layout/horizontal_grid.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid.xml
rename to leanback/tests/res/layout/horizontal_grid.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml b/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
rename to leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid_wrap.xml b/leanback/tests/res/layout/horizontal_grid_wrap.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid_wrap.xml
rename to leanback/tests/res/layout/horizontal_grid_wrap.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_item.xml b/leanback/tests/res/layout/horizontal_item.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_item.xml
rename to leanback/tests/res/layout/horizontal_item.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear.xml b/leanback/tests/res/layout/horizontal_linear.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear.xml
rename to leanback/tests/res/layout/horizontal_linear.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml b/leanback/tests/res/layout/horizontal_linear_rtl.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
rename to leanback/tests/res/layout/horizontal_linear_rtl.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_wrap_content.xml b/leanback/tests/res/layout/horizontal_linear_wrap_content.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear_wrap_content.xml
rename to leanback/tests/res/layout/horizontal_linear_wrap_content.xml
diff --git a/v17/leanback/tests/res/layout/item_button_at_bottom.xml b/leanback/tests/res/layout/item_button_at_bottom.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/item_button_at_bottom.xml
rename to leanback/tests/res/layout/item_button_at_bottom.xml
diff --git a/v17/leanback/tests/res/layout/item_button_at_top.xml b/leanback/tests/res/layout/item_button_at_top.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/item_button_at_top.xml
rename to leanback/tests/res/layout/item_button_at_top.xml
diff --git a/leanback/tests/res/layout/page_fragment.xml b/leanback/tests/res/layout/page_fragment.xml
new file mode 100644
index 0000000..9273f6f
--- /dev/null
+++ b/leanback/tests/res/layout/page_fragment.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="128dp"
+ android:layout_centerVertical="true">
+
+ <EditText
+ android:id="@+id/tv1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Header 1"
+ android:layout_margin="16dp"
+ android:focusable="true"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" />
+
+ <EditText
+ android:id="@+id/tv2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Header 2"
+ android:layout_margin="16dp"
+ android:focusable="true"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium" />
+
+ <EditText
+ android:id="@+id/tv3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Header 3"
+ android:layout_margin="16dp"
+ android:focusable="true"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" />
+
+ </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_controls_with_video.xml b/leanback/tests/res/layout/playback_controls_with_video.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/playback_controls_with_video.xml
rename to leanback/tests/res/layout/playback_controls_with_video.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout.xml b/leanback/tests/res/layout/relative_layout.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout.xml
rename to leanback/tests/res/layout/relative_layout.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout2.xml b/leanback/tests/res/layout/relative_layout2.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout2.xml
rename to leanback/tests/res/layout/relative_layout2.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout3.xml b/leanback/tests/res/layout/relative_layout3.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout3.xml
rename to leanback/tests/res/layout/relative_layout3.xml
diff --git a/v17/leanback/tests/res/layout/selectable_text_view.xml b/leanback/tests/res/layout/selectable_text_view.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/selectable_text_view.xml
rename to leanback/tests/res/layout/selectable_text_view.xml
diff --git a/v17/leanback/tests/res/layout/single_fragment.xml b/leanback/tests/res/layout/single_fragment.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/single_fragment.xml
rename to leanback/tests/res/layout/single_fragment.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_alone.xml b/leanback/tests/res/layout/timepicker_alone.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/timepicker_alone.xml
rename to leanback/tests/res/layout/timepicker_alone.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml b/leanback/tests/res/layout/timepicker_with_other_widgets.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
rename to leanback/tests/res/layout/timepicker_with_other_widgets.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid.xml b/leanback/tests/res/layout/vertical_grid.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid.xml
rename to leanback/tests/res/layout/vertical_grid.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_ltr.xml b/leanback/tests/res/layout/vertical_grid_ltr.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_ltr.xml
rename to leanback/tests/res/layout/vertical_grid_ltr.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_rtl.xml b/leanback/tests/res/layout/vertical_grid_rtl.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_rtl.xml
rename to leanback/tests/res/layout/vertical_grid_rtl.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml b/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
rename to leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear.xml b/leanback/tests/res/layout/vertical_linear.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear.xml
rename to leanback/tests/res/layout/vertical_linear.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml b/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
rename to leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_with_button.xml b/leanback/tests/res/layout/vertical_linear_with_button.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_with_button.xml
rename to leanback/tests/res/layout/vertical_linear_with_button.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml b/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
rename to leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_wrap_content.xml b/leanback/tests/res/layout/vertical_linear_wrap_content.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_wrap_content.xml
rename to leanback/tests/res/layout/vertical_linear_wrap_content.xml
diff --git a/v17/leanback/tests/res/layout/video_fragment_with_controls.xml b/leanback/tests/res/layout/video_fragment_with_controls.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/video_fragment_with_controls.xml
rename to leanback/tests/res/layout/video_fragment_with_controls.xml
diff --git a/v17/leanback/tests/res/raw/track_01.mp3 b/leanback/tests/res/raw/track_01.mp3
similarity index 100%
rename from v17/leanback/tests/res/raw/track_01.mp3
rename to leanback/tests/res/raw/track_01.mp3
Binary files differ
diff --git a/v17/leanback/tests/res/raw/video.mp4 b/leanback/tests/res/raw/video.mp4
similarity index 100%
rename from v17/leanback/tests/res/raw/video.mp4
rename to leanback/tests/res/raw/video.mp4
Binary files differ
diff --git a/v17/leanback/tests/res/values/strings.xml b/leanback/tests/res/values/strings.xml
similarity index 100%
rename from v17/leanback/tests/res/values/strings.xml
rename to leanback/tests/res/values/strings.xml
diff --git a/lifecycle/gradle/wrapper/gradle-wrapper.properties b/lifecycle/gradle/wrapper/gradle-wrapper.properties
index b519e0a..6051ae0 100644
--- a/lifecycle/gradle/wrapper/gradle-wrapper.properties
+++ b/lifecycle/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/media-compat-test-client/AndroidManifest.xml b/media-compat-test-client/AndroidManifest.xml
deleted file mode 100644
index 290b67e..0000000
--- a/media-compat-test-client/AndroidManifest.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<manifest package="android.support.mediacompat.client"/>
diff --git a/media-compat-test-client/build.gradle b/media-compat-test-client/build.gradle
deleted file mode 100644
index 47141db..0000000
--- a/media-compat-test-client/build.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
- id("SupportAndroidLibraryPlugin")
-}
-
-dependencies {
- androidTestImplementation project(':support-annotations')
- androidTestImplementation project(':support-media-compat')
- androidTestImplementation project(':support-media-compat-test-lib')
- androidTestImplementation project(':support-testutils')
-
- androidTestImplementation(libs.test_runner) {
- exclude module: 'support-annotations'
- }
-}
-
-android {
- defaultConfig {
- minSdkVersion 14
- }
-}
-
-supportLibrary {
- legacySourceLocation = true
-}
\ No newline at end of file
diff --git a/media-compat-test-client/tests/AndroidManifest.xml b/media-compat-test-client/tests/AndroidManifest.xml
deleted file mode 100644
index 8938399..0000000
--- a/media-compat-test-client/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.support.mediacompat.client.test">
- <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
-</manifest>
diff --git a/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
deleted file mode 100644
index f9f24a0..0000000
--- a/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ /dev/null
@@ -1,672 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.client;
-
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
- .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
-import static android.support.test.InstrumentationRegistry.getContext;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.support.mediacompat.client.util.IntentUtil;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.MediaBrowserServiceCompat;
-import android.support.v4.media.MediaDescriptionCompat;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserCompatTest {
-
- // The maximum time to wait for an operation.
- private static final long TIME_OUT_MS = 3000L;
-
- /**
- * To check {@link MediaBrowserCompat#unsubscribe} works properly,
- * we notify to the browser after the unsubscription that the media items have changed.
- * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
- *
- * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
- * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
- * 50ms.
- * So we make the thread sleep for 100ms to properly check that the callback is not called.
- */
- private static final long SLEEP_MS = 100L;
- private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
- "android.support.mediacompat.service.test",
- "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
- private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
- "invalid.package", "invalid.ServiceClassName");
-
- private MediaBrowserCompat mMediaBrowser;
- private StubConnectionCallback mConnectionCallback;
- private StubSubscriptionCallback mSubscriptionCallback;
- private StubItemCallback mItemCallback;
- private Bundle mRootHints;
-
- @Before
- public void setUp() {
- mConnectionCallback = new StubConnectionCallback();
- mSubscriptionCallback = new StubSubscriptionCallback();
- mItemCallback = new StubItemCallback();
-
- mRootHints = new Bundle();
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
- }
-
- @After
- public void tearDown() {
- if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
- mMediaBrowser.disconnect();
- }
- }
-
- @Test
- @SmallTest
- public void testMediaBrowser() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- assertFalse(mMediaBrowser.isConnected());
-
- connectMediaBrowserService();
- assertTrue(mMediaBrowser.isConnected());
-
- assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
- assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
- assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
-
- mMediaBrowser.disconnect();
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return !mMediaBrowser.isConnected();
- }
- }.run();
- }
-
- @Test
- @SmallTest
- public void testConnectTwice() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- try {
- mMediaBrowser.connect();
- fail();
- } catch (IllegalStateException e) {
- // expected
- }
- }
-
- @Test
- @SmallTest
- public void testConnectionFailed() throws Exception {
- createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
-
- synchronized (mConnectionCallback.mWaitLock) {
- mMediaBrowser.connect();
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- }
- assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
- assertEquals(0, mConnectionCallback.mConnectedCount);
- assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
- }
-
- @Test
- @SmallTest
- public void testReconnection() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
-
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser.connect();
- // Reconnect before the first connection was established.
- mMediaBrowser.disconnect();
- mMediaBrowser.connect();
- }
- });
-
- synchronized (mConnectionCallback.mWaitLock) {
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(1, mConnectionCallback.mConnectedCount);
- }
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- // Test subscribe.
- resetCallbacks();
- mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
- assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
- }
-
- synchronized (mItemCallback.mWaitLock) {
- // Test getItem.
- resetCallbacks();
- mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
- }
-
- // Reconnect after connection was established.
- mMediaBrowser.disconnect();
- resetCallbacks();
- connectMediaBrowserService();
-
- synchronized (mItemCallback.mWaitLock) {
- // Test getItem.
- resetCallbacks();
- mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
- }
- }
-
- @Test
- @SmallTest
- public void testConnectionCallbackNotCalledAfterDisconnect() {
- createMediaBrowser(TEST_BROWSER_SERVICE);
-
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser.connect();
- mMediaBrowser.disconnect();
- resetCallbacks();
- }
- });
-
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- assertEquals(0, mConnectionCallback.mConnectedCount);
- assertEquals(0, mConnectionCallback.mConnectionFailedCount);
- assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
- }
-
- @Test
- @SmallTest
- public void testGetServiceComponentBeforeConnection() {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- try {
- ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
- fail();
- } catch (IllegalStateException e) {
- // expected
- }
- }
-
- @Test
- @SmallTest
- public void testSubscribe() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
- assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
- assertEquals(MEDIA_ID_CHILDREN.length,
- mSubscriptionCallback.mLastChildMediaItems.size());
- for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
- assertEquals(MEDIA_ID_CHILDREN[i],
- mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
- }
-
- // Test MediaBrowserServiceCompat.notifyChildrenChanged()
- mSubscriptionCallback.reset();
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
- }
-
- // Test unsubscribe.
- resetCallbacks();
- mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- // onChildrenLoaded should not be called.
- assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
- }
-
- @Test
- @SmallTest
- public void testSubscribeWithOptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final int pageSize = 3;
- final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- for (int page = 0; page <= lastPage; ++page) {
- resetCallbacks();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
- assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
- if (page != lastPage) {
- assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
- } else {
- assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
- mSubscriptionCallback.mLastChildMediaItems.size());
- }
- // Check whether all the items in the current page are loaded.
- for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
- assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
- mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
- }
- }
-
- // Test MediaBrowserServiceCompat.notifyChildrenChanged()
- mSubscriptionCallback.reset();
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
- }
-
- // Test unsubscribe with callback argument.
- resetCallbacks();
- mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- // onChildrenLoaded should not be called.
- assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
- }
-
- @Test
- @SmallTest
- public void testSubscribeInvalidItem() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
- }
- }
-
- @Test
- @SmallTest
- public void testSubscribeInvalidItemWithOptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- final int pageSize = 5;
- final int page = 2;
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
- assertNotNull(mSubscriptionCallback.mLastOptions);
- assertEquals(page,
- mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
- assertEquals(pageSize,
- mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
- }
- }
-
- @Test
- @SmallTest
- public void testUnsubscribeForMultipleSubscriptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
- final int pageSize = 1;
-
- // Subscribe four pages, one item per page.
- for (int page = 0; page < 4; page++) {
- final StubSubscriptionCallback callback = new StubSubscriptionCallback();
- subscriptionCallbacks.add(callback);
-
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
- synchronized (callback.mWaitLock) {
- callback.mWaitLock.wait(TIME_OUT_MS);
- }
- // Each onChildrenLoaded() must be called.
- assertEquals(1, callback.mChildrenLoadedWithOptionCount);
- }
-
- // Reset callbacks and unsubscribe.
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- callback.reset();
- }
- mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
-
- // onChildrenLoaded should not be called.
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- assertEquals(0, callback.mChildrenLoadedWithOptionCount);
- }
- }
-
- @Test
- @MediumTest
- public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
- final int pageSize = 1;
-
- // Subscribe four pages, one item per page.
- for (int page = 0; page < 4; page++) {
- final StubSubscriptionCallback callback = new StubSubscriptionCallback();
- subscriptionCallbacks.add(callback);
-
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- mMediaBrowser.subscribe(MEDIA_ID_ROOT, options,
- callback);
- synchronized (callback.mWaitLock) {
- callback.mWaitLock.wait(TIME_OUT_MS);
- }
- // Each onChildrenLoaded() must be called.
- assertEquals(1, callback.mChildrenLoadedWithOptionCount);
- }
-
- // Unsubscribe existing subscriptions one-by-one.
- final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
- for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
- // Reset callbacks
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- callback.reset();
- }
-
- // Remove one subscription
- mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
- subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
-
- // Make StubMediaBrowserServiceCompat notify that the children are changed.
- callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
-
- // Only the remaining subscriptionCallbacks should be called.
- for (int j = 0; j < 4; j++) {
- int childrenLoadedWithOptionsCount = subscriptionCallbacks
- .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
- if (j <= i) {
- assertEquals(0, childrenLoadedWithOptionsCount);
- } else {
- assertEquals(1, childrenLoadedWithOptionsCount);
- }
- }
- }
- }
-
- @Test
- @SmallTest
- public void testGetItem() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertNotNull(mItemCallback.mLastMediaItem);
- assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
- }
- }
-
- @Test
- @LargeTest
- public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
- }
- }
-
- @Test
- @SmallTest
- public void testGetItemWhenMediaIdIsInvalid() throws Exception {
- mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
-
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertNull(mItemCallback.mLastMediaItem);
- assertNull(mItemCallback.mLastErrorId);
- }
- }
-
- private void createMediaBrowser(final ComponentName component) {
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
- component, mConnectionCallback, mRootHints);
- }
- });
- }
-
- private void connectMediaBrowserService() throws Exception {
- synchronized (mConnectionCallback.mWaitLock) {
- mMediaBrowser.connect();
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- if (!mMediaBrowser.isConnected()) {
- fail("Browser failed to connect!");
- }
- }
- }
-
- private void callMediaBrowserServiceMethod(int methodId, Object arg) {
- IntentUtil.callMediaBrowserServiceMethod(methodId, arg, getContext());
- }
-
- private void resetCallbacks() {
- mConnectionCallback.reset();
- mSubscriptionCallback.reset();
- mItemCallback.reset();
- }
-
- private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
- Object mWaitLock = new Object();
- volatile int mConnectedCount;
- volatile int mConnectionFailedCount;
- volatile int mConnectionSuspendedCount;
-
- public void reset() {
- mConnectedCount = 0;
- mConnectionFailedCount = 0;
- mConnectionSuspendedCount = 0;
- }
-
- @Override
- public void onConnected() {
- synchronized (mWaitLock) {
- mConnectedCount++;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onConnectionFailed() {
- synchronized (mWaitLock) {
- mConnectionFailedCount++;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- synchronized (mWaitLock) {
- mConnectionSuspendedCount++;
- mWaitLock.notify();
- }
- }
- }
-
- private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
- final Object mWaitLock = new Object();
- private volatile int mChildrenLoadedCount;
- private volatile int mChildrenLoadedWithOptionCount;
- private volatile String mLastErrorId;
- private volatile String mLastParentId;
- private volatile Bundle mLastOptions;
- private volatile List<MediaItem> mLastChildMediaItems;
-
- public void reset() {
- mChildrenLoadedCount = 0;
- mChildrenLoadedWithOptionCount = 0;
- mLastErrorId = null;
- mLastParentId = null;
- mLastOptions = null;
- mLastChildMediaItems = null;
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children) {
- synchronized (mWaitLock) {
- mChildrenLoadedCount++;
- mLastParentId = parentId;
- mLastChildMediaItems = children;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
- synchronized (mWaitLock) {
- mChildrenLoadedWithOptionCount++;
- mLastParentId = parentId;
- mLastOptions = options;
- mLastChildMediaItems = children;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id, Bundle options) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mLastOptions = options;
- mWaitLock.notify();
- }
- }
- }
-
- private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
- final Object mWaitLock = new Object();
- private volatile MediaItem mLastMediaItem;
- private volatile String mLastErrorId;
-
- public void reset() {
- mLastMediaItem = null;
- mLastErrorId = null;
- }
-
- @Override
- public void onItemLoaded(MediaItem item) {
- synchronized (mWaitLock) {
- mLastMediaItem = item;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mWaitLock.notify();
- }
- }
- }
-}
diff --git a/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java b/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java
deleted file mode 100644
index bcf33a4..0000000
--- a/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.client.util;
-
-import static android.support.mediacompat.testlib.IntentConstants
- .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_ARGUMENT;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_METHOD_ID;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-
-import java.util.ArrayList;
-
-public class IntentUtil {
-
- public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName(
- "android.support.mediacompat.service.test",
- "android.support.mediacompat.service.ServiceBroadcastReceiver");
-
- public static void callMediaBrowserServiceMethod(int methodId, Object arg, Context context) {
- Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
- intent.setAction(ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD);
- if (Build.VERSION.SDK_INT >= 16) {
- intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- }
- context.sendBroadcast(intent);
- }
-
- private static Intent createIntent(ComponentName componentName, int methodId, Object arg) {
- Intent intent = new Intent();
- intent.setComponent(componentName);
- intent.putExtra(KEY_METHOD_ID, methodId);
-
- if (arg instanceof String) {
- intent.putExtra(KEY_ARGUMENT, (String) arg);
- } else if (arg instanceof Integer) {
- intent.putExtra(KEY_ARGUMENT, (int) arg);
- } else if (arg instanceof Long) {
- intent.putExtra(KEY_ARGUMENT, (long) arg);
- } else if (arg instanceof Boolean) {
- intent.putExtra(KEY_ARGUMENT, (boolean) arg);
- } else if (arg instanceof Parcelable) {
- intent.putExtra(KEY_ARGUMENT, (Parcelable) arg);
- } else if (arg instanceof ArrayList<?>) {
- Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(KEY_ARGUMENT, (ArrayList<? extends Parcelable>) arg);
- intent.putExtras(bundle);
- } else if (arg instanceof Bundle) {
- Bundle bundle = new Bundle();
- bundle.putBundle(KEY_ARGUMENT, (Bundle) arg);
- intent.putExtras(bundle);
- }
- return intent;
- }
-}
diff --git a/media-compat-test-lib/OWNERS b/media-compat-test-lib/OWNERS
deleted file mode 100644
index 5529026..0000000
--- a/media-compat-test-lib/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-hdmoon@google.com
-sungsoo@google.com
\ No newline at end of file
diff --git a/media-compat-test-lib/build.gradle b/media-compat-test-lib/build.gradle
deleted file mode 100644
index 26594e5..0000000
--- a/media-compat-test-lib/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-apply plugin: 'java'
diff --git a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java b/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java
deleted file mode 100644
index bc35935..0000000
--- a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.testlib;
-
-/**
- * Constants used for sending intent between client and service apks.
- */
-public class IntentConstants {
- public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD =
- "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD";
- public static final String KEY_METHOD_ID = "method_id";
- public static final String KEY_ARGUMENT = "argument";
-}
diff --git a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java b/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
deleted file mode 100644
index 8ef0a35..0000000
--- a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.testlib;
-
-/**
- * Constants for testing the media browser and service.
- */
-public class MediaBrowserConstants {
-
- // MediaBrowserServiceCompat methods.
- public static final int NOTIFY_CHILDREN_CHANGED = 1;
- public static final int SEND_DELAYED_NOTIFY_CHILDREN_CHANGED = 2;
- public static final int SEND_DELAYED_ITEM_LOADED = 3;
- public static final int CUSTOM_ACTION_SEND_PROGRESS_UPDATE = 4;
- public static final int CUSTOM_ACTION_SEND_ERROR = 5;
- public static final int CUSTOM_ACTION_SEND_RESULT = 6;
- public static final int SET_SESSION_TOKEN = 7;
-
- public static final String MEDIA_ID_ROOT = "test_media_id_root";
-
- public static final String EXTRAS_KEY = "test_extras_key";
- public static final String EXTRAS_VALUE = "test_extras_value";
-
- public static final String MEDIA_ID_INVALID = "test_media_id_invalid";
- public static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed";
- public static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED =
- "test_media_id_on_load_item_not_implemented";
-
- public static final String SEARCH_QUERY = "children_2";
- public static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
- public static final String SEARCH_QUERY_FOR_ERROR = "query for error";
-
- public static final String CUSTOM_ACTION = "CUSTOM_ACTION";
- public static final String CUSTOM_ACTION_FOR_ERROR = "CUSTOM_ACTION_FOR_ERROR";
-
- public static final String TEST_KEY_1 = "key_1";
- public static final String TEST_VALUE_1 = "value_1";
- public static final String TEST_KEY_2 = "key_2";
- public static final String TEST_VALUE_2 = "value_2";
- public static final String TEST_KEY_3 = "key_3";
- public static final String TEST_VALUE_3 = "value_3";
- public static final String TEST_KEY_4 = "key_4";
- public static final String TEST_VALUE_4 = "value_4";
-
- public static final String[] MEDIA_ID_CHILDREN = new String[]{
- "test_media_id_children_0", "test_media_id_children_1",
- "test_media_id_children_2", "test_media_id_children_3",
- MEDIA_ID_CHILDREN_DELAYED
- };
-}
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat-test-service/AndroidManifest.xml
deleted file mode 100644
index 0390e8a..0000000
--- a/media-compat-test-service/AndroidManifest.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<manifest package="android.support.mediacompat.service"/>
diff --git a/media-compat-test-service/OWNERS b/media-compat-test-service/OWNERS
deleted file mode 100644
index 5529026..0000000
--- a/media-compat-test-service/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-hdmoon@google.com
-sungsoo@google.com
\ No newline at end of file
diff --git a/media-compat-test-service/build.gradle b/media-compat-test-service/build.gradle
deleted file mode 100644
index 946d48b..0000000
--- a/media-compat-test-service/build.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
- id("SupportAndroidLibraryPlugin")
-}
-
-dependencies {
- androidTestImplementation project(':support-annotations')
- androidTestImplementation project(':support-media-compat')
- androidTestImplementation project(':support-media-compat-test-lib')
-
- androidTestImplementation(libs.test_runner) {
- exclude module: 'support-annotations'
- }
-}
-
-android {
- defaultConfig {
- minSdkVersion 14
- }
-}
-
-supportLibrary {
- legacySourceLocation = true
-}
diff --git a/media-compat-test-service/lint-baseline.xml b/media-compat-test-service/lint-baseline.xml
deleted file mode 100644
index 8bc6f6f..0000000
--- a/media-compat-test-service/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-alpha9">
-
-</issues>
diff --git a/media-compat-test-service/tests/AndroidManifest.xml b/media-compat-test-service/tests/AndroidManifest.xml
deleted file mode 100644
index 3e1eff9..0000000
--- a/media-compat-test-service/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.support.mediacompat.service.test">
- <application>
- <receiver android:name="android.support.mediacompat.service.ServiceBroadcastReceiver">
- <intent-filter>
- <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"/>
- </intent-filter>
- </receiver>
-
- <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
- <intent-filter>
- <action android:name="android.intent.action.MEDIA_BUTTON" />
- </intent-filter>
- </receiver>
-
- <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompat">
- <intent-filter>
- <action android:name="android.media.browse.MediaBrowserService"/>
- </intent-filter>
- </service>
-
- <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompatWithDelayedMediaSession">
- <intent-filter>
- <action android:name="android.media.browse.MediaBrowserService"/>
- </intent-filter>
- </service>
- </application>
-</manifest>
diff --git a/media-compat-test-service/tests/NO_DOCS b/media-compat-test-service/tests/NO_DOCS
deleted file mode 100644
index 4dad694..0000000
--- a/media-compat-test-service/tests/NO_DOCS
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-Having this file, named NO_DOCS, in a directory will prevent
-Android javadocs from being generated for java files under
-the directory. This is especially useful for test projects.
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
deleted file mode 100644
index d987fd8..0000000
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.service;
-
-
-import static android.support.mediacompat.testlib.IntentConstants
- .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_ARGUMENT;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_METHOD_ID;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
- .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
- .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-public class ServiceBroadcastReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Bundle extras = intent.getExtras();
- if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
- StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
- int method = extras.getInt(KEY_METHOD_ID, 0);
-
- switch (method) {
- case NOTIFY_CHILDREN_CHANGED:
- service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
- break;
- case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
- service.sendDelayedNotifyChildrenChanged();
- break;
- case SEND_DELAYED_ITEM_LOADED:
- service.sendDelayedItemLoaded();
- break;
- case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
- service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
- break;
- case CUSTOM_ACTION_SEND_ERROR:
- service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
- break;
- case CUSTOM_ACTION_SEND_RESULT:
- service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
- break;
- case SET_SESSION_TOKEN:
- StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
- .callSetSessionToken();
- break;
- }
- }
- }
-}
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
deleted file mode 100644
index fa9f1c5..0000000
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.service;
-
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.MediaBrowserServiceCompat;
-import android.support.v4.media.MediaDescriptionCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import junit.framework.Assert;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
-
- public static StubMediaBrowserServiceCompat sInstance;
-
- public static MediaSessionCompat sSession;
- private Bundle mExtras;
- private Result<List<MediaItem>> mPendingLoadChildrenResult;
- private Result<MediaItem> mPendingLoadItemResult;
- private Bundle mPendingRootHints;
-
- public Bundle mCustomActionExtras;
- public Result<Bundle> mCustomActionResult;
-
- @Override
- public void onCreate() {
- super.onCreate();
- sInstance = this;
- sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
- setSessionToken(sSession.getSessionToken());
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- sSession.release();
- sSession = null;
- }
-
- @Override
- public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
- mExtras = new Bundle();
- mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
- return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
- }
-
- @Override
- public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
- List<MediaItem> mediaItems = new ArrayList<>();
- if (MEDIA_ID_ROOT.equals(parentMediaId)) {
- Bundle rootHints = getBrowserRootHints();
- for (String id : MEDIA_ID_CHILDREN) {
- mediaItems.add(createMediaItem(id));
- }
- result.sendResult(mediaItems);
- } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
- Assert.assertNull(mPendingLoadChildrenResult);
- mPendingLoadChildrenResult = result;
- mPendingRootHints = getBrowserRootHints();
- result.detach();
- } else if (MEDIA_ID_INVALID.equals(parentMediaId)) {
- result.sendResult(null);
- }
- }
-
- @Override
- public void onLoadItem(String itemId, Result<MediaItem> result) {
- if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
- mPendingLoadItemResult = result;
- mPendingRootHints = getBrowserRootHints();
- result.detach();
- return;
- }
-
- if (MEDIA_ID_INVALID.equals(itemId)) {
- result.sendResult(null);
- return;
- }
-
- for (String id : MEDIA_ID_CHILDREN) {
- if (id.equals(itemId)) {
- result.sendResult(createMediaItem(id));
- return;
- }
- }
-
- // Test the case where onLoadItem is not implemented.
- super.onLoadItem(itemId, result);
- }
-
- @Override
- public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
- if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
- result.sendResult(Collections.<MediaItem>emptyList());
- } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
- result.sendResult(null);
- } else if (SEARCH_QUERY.equals(query)) {
- List<MediaItem> items = new ArrayList<>();
- for (String id : MEDIA_ID_CHILDREN) {
- if (id.contains(query)) {
- items.add(createMediaItem(id));
- }
- }
- result.sendResult(items);
- }
- }
-
- @Override
- public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
- mCustomActionResult = result;
- mCustomActionExtras = extras;
- if (CUSTOM_ACTION_FOR_ERROR.equals(action)) {
- result.sendError(null);
- } else if (CUSTOM_ACTION.equals(action)) {
- result.detach();
- }
- }
-
- public void sendDelayedNotifyChildrenChanged() {
- if (mPendingLoadChildrenResult != null) {
- mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
- mPendingRootHints = null;
- mPendingLoadChildrenResult = null;
- }
- }
-
- public void sendDelayedItemLoaded() {
- if (mPendingLoadItemResult != null) {
- mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
- MediaItem.FLAG_BROWSABLE));
- mPendingRootHints = null;
- mPendingLoadItemResult = null;
- }
- }
-
- private MediaItem createMediaItem(String id) {
- return new MediaItem(new MediaDescriptionCompat.Builder().setMediaId(id).build(),
- MediaItem.FLAG_BROWSABLE);
- }
-}
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
deleted file mode 100644
index 12cb358..0000000
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.mediacompat.service;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.MediaBrowserServiceCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import java.util.List;
-
-/**
- * Stub implementation of {@link MediaBrowserServiceCompat}.
- * This implementation does not call
- * {@link MediaBrowserServiceCompat#setSessionToken(MediaSessionCompat.Token)} in its
- * {@link android.app.Service#onCreate}.
- */
-public class StubMediaBrowserServiceCompatWithDelayedMediaSession extends
- MediaBrowserServiceCompat {
-
- static StubMediaBrowserServiceCompatWithDelayedMediaSession sInstance;
- private MediaSessionCompat mSession;
-
- @Override
- public void onCreate() {
- super.onCreate();
- sInstance = this;
- mSession = new MediaSessionCompat(
- this, "StubMediaBrowserServiceCompatWithDelayedMediaSession");
- }
-
- @Nullable
- @Override
- public BrowserRoot onGetRoot(@NonNull String clientPackageName,
- int clientUid, @Nullable Bundle rootHints) {
- return new BrowserRoot("StubRootId", null);
- }
-
- @Override
- public void onLoadChildren(@NonNull String parentId,
- @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
- result.detach();
- }
-
- public void callSetSessionToken() {
- setSessionToken(mSession.getSessionToken());
- }
-}
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index 6d9a4f6..09c004d 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -24,23 +24,6 @@
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
- <service android:name="android.support.v4.media.StubMediaBrowserServiceCompat">
- <intent-filter>
- <action android:name="android.media.browse.MediaBrowserService"/>
- </intent-filter>
- </service>
- <service android:name="android.support.v4.media.StubRemoteMediaBrowserServiceCompat"
- android:process=":remote">
- <intent-filter>
- <action android:name="android.media.browse.MediaBrowserService"/>
- </intent-filter>
- </service>
- <service
- android:name="android.support.v4.media.StubMediaBrowserServiceCompatWithDelayedMediaSession">
- <intent-filter>
- <action android:name="android.media.browse.MediaBrowserService"/>
- </intent-filter>
- </service>
</application>
</manifest>
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
deleted file mode 100644
index 6356e33..0000000
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ /dev/null
@@ -1,786 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media;
-
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserCompatTest {
-
- // The maximum time to wait for an operation.
- private static final long TIME_OUT_MS = 3000L;
-
- /**
- * To check {@link MediaBrowserCompat#unsubscribe} works properly,
- * we notify to the browser after the unsubscription that the media items have changed.
- * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
- *
- * The measured time from calling {@link StubMediaBrowserServiceCompat#notifyChildrenChanged}
- * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
- * 50ms.
- * So we make the thread sleep for 100ms to properly check that the callback is not called.
- */
- private static final long SLEEP_MS = 100L;
- private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
- "android.support.mediacompat.test",
- "android.support.v4.media.StubMediaBrowserServiceCompat");
- private static final ComponentName TEST_REMOTE_BROWSER_SERVICE = new ComponentName(
- "android.support.mediacompat.test",
- "android.support.v4.media.StubRemoteMediaBrowserServiceCompat");
- private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
- "invalid.package", "invalid.ServiceClassName");
-
- private MediaBrowserCompat mMediaBrowser;
- private StubConnectionCallback mConnectionCallback;
- private StubSubscriptionCallback mSubscriptionCallback;
- private StubItemCallback mItemCallback;
-
- @Before
- public void setUp() {
- mConnectionCallback = new StubConnectionCallback();
- mSubscriptionCallback = new StubSubscriptionCallback();
- mItemCallback = new StubItemCallback();
- }
-
- @Test
- @SmallTest
- public void testMediaBrowser() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- assertFalse(mMediaBrowser.isConnected());
-
- connectMediaBrowserService();
- assertTrue(mMediaBrowser.isConnected());
-
- assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
- assertEquals(StubMediaBrowserServiceCompat.EXTRAS_VALUE,
- mMediaBrowser.getExtras().getString(StubMediaBrowserServiceCompat.EXTRAS_KEY));
- assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
- mMediaBrowser.getSessionToken());
-
- mMediaBrowser.disconnect();
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return !mMediaBrowser.isConnected();
- }
- }.run();
- }
-
- @Test
- @SmallTest
- public void testMediaBrowserWithRemoteService() throws Exception {
- createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
- assertFalse(mMediaBrowser.isConnected());
-
- connectMediaBrowserService();
- assertTrue(mMediaBrowser.isConnected());
-
- assertEquals(TEST_REMOTE_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
- assertEquals(StubRemoteMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
- assertEquals(StubRemoteMediaBrowserServiceCompat.EXTRAS_VALUE,
- mMediaBrowser.getExtras().getString(
- StubRemoteMediaBrowserServiceCompat.EXTRAS_KEY));
-
- mMediaBrowser.disconnect();
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return !mMediaBrowser.isConnected();
- }
- }.run();
- }
-
- @Test
- @SmallTest
- public void testSessionReadyWithRemoteService() throws Exception {
- if (android.os.Build.VERSION.SDK_INT < 21) {
- // This test is for API 21+
- return;
- }
-
- createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
- assertFalse(mMediaBrowser.isConnected());
-
- connectMediaBrowserService();
- assertTrue(mMediaBrowser.isConnected());
-
- // Create a session token by removing the extra binder of the token from MediaBrowserCompat.
- final MediaSessionCompat.Token tokenWithoutExtraBinder = MediaSessionCompat.Token.fromToken(
- mMediaBrowser.getSessionToken().getToken());
-
- final MediaControllerCallback callback = new MediaControllerCallback();
- synchronized (callback.mWaitLock) {
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- try {
- MediaControllerCompat controller = new MediaControllerCompat(
- getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
- controller.registerCallback(callback, new Handler());
- assertFalse(controller.isSessionReady());
- } catch (Exception e) {
- fail();
- }
- }
- });
- callback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(callback.mOnSessionReadyCalled);
- }
-
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testSubscriptionWithCustomOptionsWithRemoteService() throws Exception {
- final String mediaId = "1000";
- createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
- assertFalse(mMediaBrowser.isConnected());
-
- connectMediaBrowserService();
- assertTrue(mMediaBrowser.isConnected());
-
- MediaMetadataCompat mediaMetadataCompat = new MediaMetadataCompat.Builder()
- .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
- .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
- .putRating(MediaMetadataCompat.METADATA_KEY_RATING,
- RatingCompat.newPercentageRating(0.5f))
- .putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING,
- RatingCompat.newPercentageRating(0.8f))
- .build();
- Bundle options = new Bundle();
- options.putParcelable(
- StubRemoteMediaBrowserServiceCompat.MEDIA_METADATA, mediaMetadataCompat);
-
- // Remote MediaBrowserService will create a media item with the given MediaMetadataCompat
- mMediaBrowser.subscribe(mMediaBrowser.getRoot(), options, mSubscriptionCallback);
- synchronized (mSubscriptionCallback.mWaitLock) {
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
- assertEquals(1, mSubscriptionCallback.mLastChildMediaItems.size());
- assertEquals(mediaId, mSubscriptionCallback.mLastChildMediaItems.get(0).getMediaId());
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testConnectTwice() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- try {
- mMediaBrowser.connect();
- fail();
- } catch (IllegalStateException e) {
- // expected
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testConnectionFailed() throws Exception {
- createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
-
- synchronized (mConnectionCallback.mWaitLock) {
- mMediaBrowser.connect();
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- }
- assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
- assertEquals(0, mConnectionCallback.mConnectedCount);
- assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testReconnection() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
-
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser.connect();
- // Reconnect before the first connection was established.
- mMediaBrowser.disconnect();
- mMediaBrowser.connect();
- }
- });
-
- synchronized (mConnectionCallback.mWaitLock) {
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(1, mConnectionCallback.mConnectedCount);
- }
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- // Test subscribe.
- resetCallbacks();
- mMediaBrowser.subscribe(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
- mSubscriptionCallback.mLastParentId);
- }
-
- synchronized (mItemCallback.mWaitLock) {
- // Test getItem.
- resetCallbacks();
- mMediaBrowser.getItem(
- StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0], mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
- mItemCallback.mLastMediaItem.getMediaId());
- }
-
- // Reconnect after connection was established.
- mMediaBrowser.disconnect();
- resetCallbacks();
- connectMediaBrowserService();
-
- synchronized (mItemCallback.mWaitLock) {
- // Test getItem.
- resetCallbacks();
- mMediaBrowser.getItem(
- StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0], mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
- mItemCallback.mLastMediaItem.getMediaId());
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testConnectionCallbackNotCalledAfterDisconnect() {
- createMediaBrowser(TEST_BROWSER_SERVICE);
-
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser.connect();
- mMediaBrowser.disconnect();
- resetCallbacks();
- }
- });
-
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- assertEquals(0, mConnectionCallback.mConnectedCount);
- assertEquals(0, mConnectionCallback.mConnectionFailedCount);
- assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
- }
-
- @Test
- @SmallTest
- public void testGetServiceComponentBeforeConnection() {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- try {
- ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
- fail();
- } catch (IllegalStateException e) {
- // expected
- }
- }
-
- @Test
- @SmallTest
- public void testSubscribe() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
- mSubscriptionCallback.mLastParentId);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length,
- mSubscriptionCallback.mLastChildMediaItems.size());
- for (int i = 0; i < StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length; ++i) {
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[i],
- mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
- }
- }
-
- // Test unsubscribe.
- resetCallbacks();
- mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- // onChildrenLoaded should not be called.
- assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testSubscribeWithOptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final int pageSize = 3;
- final int lastPage =
- (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) / pageSize;
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- for (int page = 0; page <= lastPage; ++page) {
- resetCallbacks();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
- mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
- mSubscriptionCallback.mLastParentId);
- if (page != lastPage) {
- assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
- } else {
- assertEquals(
- (StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN.length - 1) % pageSize
- + 1,
- mSubscriptionCallback.mLastChildMediaItems.size());
- }
- // Check whether all the items in the current page are loaded.
- for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
- assertEquals(
- StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[page * pageSize + i],
- mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
- }
- }
- }
-
- // Test unsubscribe with callback argument.
- resetCallbacks();
- mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
- mSubscriptionCallback);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
- // onChildrenLoaded should not be called.
- assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testSubscribeInvalidItem() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
- mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
- mSubscriptionCallback.mLastErrorId);
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testSubscribeInvalidItemWithOptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- final int pageSize = 5;
- final int page = 2;
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
- synchronized (mSubscriptionCallback.mWaitLock) {
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, options,
- mSubscriptionCallback);
- mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID,
- mSubscriptionCallback.mLastErrorId);
- assertNotNull(mSubscriptionCallback.mLastOptions);
- assertEquals(page,
- mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
- assertEquals(pageSize,
- mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testUnsubscribeForMultipleSubscriptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
- final int pageSize = 1;
-
- // Subscribe four pages, one item per page.
- for (int page = 0; page < 4; page++) {
- final StubSubscriptionCallback callback = new StubSubscriptionCallback();
- subscriptionCallbacks.add(callback);
-
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
- callback);
- synchronized (callback.mWaitLock) {
- callback.mWaitLock.wait(TIME_OUT_MS);
- }
- // Each onChildrenLoaded() must be called.
- assertEquals(1, callback.mChildrenLoadedWithOptionCount);
- }
-
- // Reset callbacks and unsubscribe.
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- callback.reset();
- }
- mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
-
- // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
- // changed.
- StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
-
- // onChildrenLoaded should not be called.
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- assertEquals(0, callback.mChildrenLoadedWithOptionCount);
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @MediumTest
- public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
- final int pageSize = 1;
-
- // Subscribe four pages, one item per page.
- for (int page = 0; page < 4; page++) {
- final StubSubscriptionCallback callback = new StubSubscriptionCallback();
- subscriptionCallbacks.add(callback);
-
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
- callback);
- synchronized (callback.mWaitLock) {
- callback.mWaitLock.wait(TIME_OUT_MS);
- }
- // Each onChildrenLoaded() must be called.
- assertEquals(1, callback.mChildrenLoadedWithOptionCount);
- }
-
- // Unsubscribe existing subscriptions one-by-one.
- final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
- for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
- // Reset callbacks
- for (StubSubscriptionCallback callback : subscriptionCallbacks) {
- callback.reset();
- }
-
- // Remove one subscription
- mMediaBrowser.unsubscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT,
- subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
-
- // Make StubMediaBrowserServiceCompat notify that the children are changed.
- StubMediaBrowserServiceCompat.sInstance.notifyChildrenChanged(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- try {
- Thread.sleep(SLEEP_MS);
- } catch (InterruptedException e) {
- fail("Unexpected InterruptedException occurred.");
- }
-
- // Only the remaining subscriptionCallbacks should be called.
- for (int j = 0; j < 4; j++) {
- int childrenLoadedWithOptionsCount = subscriptionCallbacks
- .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
- if (j <= i) {
- assertEquals(0, childrenLoadedWithOptionsCount);
- } else {
- assertEquals(1, childrenLoadedWithOptionsCount);
- }
- }
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testGetItem() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
-
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
- mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertNotNull(mItemCallback.mLastMediaItem);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN[0],
- mItemCallback.mLastMediaItem.getMediaId());
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @LargeTest
- public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(
- StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
- mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED,
- mItemCallback.mLastErrorId);
- }
- mMediaBrowser.disconnect();
- }
-
- @Test
- @SmallTest
- public void testGetItemWhenMediaIdIsInvalid() throws Exception {
- mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
-
- createMediaBrowser(TEST_BROWSER_SERVICE);
- connectMediaBrowserService();
- synchronized (mItemCallback.mWaitLock) {
- mMediaBrowser.getItem(StubMediaBrowserServiceCompat.MEDIA_ID_INVALID, mItemCallback);
- mItemCallback.mWaitLock.wait(TIME_OUT_MS);
- assertNull(mItemCallback.mLastMediaItem);
- assertNull(mItemCallback.mLastErrorId);
- }
- mMediaBrowser.disconnect();
- }
-
- private void createMediaBrowser(final ComponentName component) {
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
- component, mConnectionCallback, null);
- }
- });
- }
-
- private void connectMediaBrowserService() throws Exception {
- synchronized (mConnectionCallback.mWaitLock) {
- mMediaBrowser.connect();
- mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
- }
- }
-
- private void resetCallbacks() {
- mConnectionCallback.reset();
- mSubscriptionCallback.reset();
- mItemCallback.reset();
- }
-
- private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
- Object mWaitLock = new Object();
- volatile int mConnectedCount;
- volatile int mConnectionFailedCount;
- volatile int mConnectionSuspendedCount;
-
- public void reset() {
- mConnectedCount = 0;
- mConnectionFailedCount = 0;
- mConnectionSuspendedCount = 0;
- }
-
- @Override
- public void onConnected() {
- synchronized (mWaitLock) {
- mConnectedCount++;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onConnectionFailed() {
- synchronized (mWaitLock) {
- mConnectionFailedCount++;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- synchronized (mWaitLock) {
- mConnectionSuspendedCount++;
- mWaitLock.notify();
- }
- }
- }
-
- private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
- Object mWaitLock = new Object();
- private volatile int mChildrenLoadedCount;
- private volatile int mChildrenLoadedWithOptionCount;
- private volatile String mLastErrorId;
- private volatile String mLastParentId;
- private volatile Bundle mLastOptions;
- private volatile List<MediaItem> mLastChildMediaItems;
-
- public void reset() {
- mChildrenLoadedCount = 0;
- mChildrenLoadedWithOptionCount = 0;
- mLastErrorId = null;
- mLastParentId = null;
- mLastOptions = null;
- mLastChildMediaItems = null;
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children) {
- synchronized (mWaitLock) {
- mChildrenLoadedCount++;
- mLastParentId = parentId;
- mLastChildMediaItems = children;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
- synchronized (mWaitLock) {
- mChildrenLoadedWithOptionCount++;
- mLastParentId = parentId;
- mLastOptions = options;
- mLastChildMediaItems = children;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id, Bundle options) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mLastOptions = options;
- mWaitLock.notify();
- }
- }
- }
-
- private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
- Object mWaitLock = new Object();
- private volatile MediaItem mLastMediaItem;
- private volatile String mLastErrorId;
-
- public void reset() {
- mLastMediaItem = null;
- mLastErrorId = null;
- }
-
- @Override
- public void onItemLoaded(MediaItem item) {
- synchronized (mWaitLock) {
- mLastMediaItem = item;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String id) {
- synchronized (mWaitLock) {
- mLastErrorId = id;
- mWaitLock.notify();
- }
- }
- }
-
- private class MediaControllerCallback extends MediaControllerCompat.Callback {
- Object mWaitLock = new Object();
- private volatile boolean mOnSessionReadyCalled;
-
- @Override
- public void onSessionReady() {
- synchronized (mWaitLock) {
- mOnSessionReadyCalled = true;
- mWaitLock.notify();
- }
- }
- }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
deleted file mode 100644
index 4ceac10..0000000
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
+++ /dev/null
@@ -1,586 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v4.media;
-
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.ComponentName;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserServiceCompatTest {
- // The maximum time to wait for an operation.
- private static final long TIME_OUT_MS = 3000L;
- private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
- private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
- "android.support.mediacompat.test",
- "android.support.v4.media.StubMediaBrowserServiceCompat");
- private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
- new ComponentName(
- "android.support.mediacompat.test",
- "android.support.v4.media"
- + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
- private static final String TEST_KEY_1 = "key_1";
- private static final String TEST_VALUE_1 = "value_1";
- private static final String TEST_KEY_2 = "key_2";
- private static final String TEST_VALUE_2 = "value_2";
- private static final String TEST_KEY_3 = "key_3";
- private static final String TEST_VALUE_3 = "value_3";
- private static final String TEST_KEY_4 = "key_4";
- private static final String TEST_VALUE_4 = "value_4";
- private final Object mWaitLock = new Object();
-
- private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
- private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
- private final ItemCallback mItemCallback = new ItemCallback();
- private final SearchCallback mSearchCallback = new SearchCallback();
-
- private MediaBrowserCompat mMediaBrowser;
- private MediaBrowserCompat mMediaBrowserForDelayedMediaSession;
- private StubMediaBrowserServiceCompat mMediaBrowserService;
- private Bundle mRootHints;
-
- @Before
- public void setUp() throws Exception {
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mRootHints = new Bundle();
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
- mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
- mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
- TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
- }
- });
- synchronized (mWaitLock) {
- mMediaBrowser.connect();
- mWaitLock.wait(TIME_OUT_MS);
- }
- assertNotNull(mMediaBrowserService);
- mMediaBrowserService.mCustomActionExtras = null;
- mMediaBrowserService.mCustomActionResult = null;
- }
-
- @Test
- @SmallTest
- public void testGetSessionToken() {
- assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
- mMediaBrowserService.getSessionToken());
- }
-
- @Test
- @SmallTest
- public void testNotifyChildrenChanged() throws Exception {
- synchronized (mWaitLock) {
- mSubscriptionCallback.reset();
- mMediaBrowser.subscribe(
- StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-
- mSubscriptionCallback.reset();
- mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
- }
- }
-
- @Test
- @SmallTest
- public void testNotifyChildrenChangedWithPagination() throws Exception {
- synchronized (mWaitLock) {
- final int pageSize = 5;
- final int page = 2;
- Bundle options = new Bundle();
- options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
- mSubscriptionCallback.reset();
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
- mSubscriptionCallback);
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
-
- mSubscriptionCallback.reset();
- mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
- }
- }
-
- @Test
- @MediumTest
- public void testDelayedNotifyChildrenChanged() throws Exception {
- synchronized (mWaitLock) {
- mSubscriptionCallback.reset();
- mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED,
- mSubscriptionCallback);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
-
- mMediaBrowserService.sendDelayedNotifyChildrenChanged();
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
-
- mSubscriptionCallback.reset();
- mMediaBrowserService.notifyChildrenChanged(
- StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
-
- mMediaBrowserService.sendDelayedNotifyChildrenChanged();
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
- }
- }
-
- @Test
- @MediumTest
- public void testDelayedItem() throws Exception {
- synchronized (mWaitLock) {
- mItemCallback.reset();
- mMediaBrowser.getItem(
- StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertFalse(mItemCallback.mOnItemLoaded);
-
- mItemCallback.reset();
- mMediaBrowserService.sendDelayedItemLoaded();
- mWaitLock.wait(TIME_OUT_MS);
- assertTrue(mItemCallback.mOnItemLoaded);
- }
- }
-
- @Test
- @SmallTest
- public void testSearch() throws Exception {
- final String key = "test-key";
- final String val = "test-val";
-
- synchronized (mWaitLock) {
- mSearchCallback.reset();
- mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
- mSearchCallback);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(mSearchCallback.mOnSearchResult);
- assertTrue(mSearchCallback.mSearchResults != null
- && mSearchCallback.mSearchResults.size() == 0);
- assertEquals(null, mSearchCallback.mSearchExtras);
-
- mSearchCallback.reset();
- mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
- mSearchCallback);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(mSearchCallback.mOnSearchResult);
- assertNull(mSearchCallback.mSearchResults);
- assertEquals(null, mSearchCallback.mSearchExtras);
-
- mSearchCallback.reset();
- Bundle extras = new Bundle();
- extras.putString(key, val);
- mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
- mSearchCallback);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(mSearchCallback.mOnSearchResult);
- assertNotNull(mSearchCallback.mSearchResults);
- for (MediaItem item : mSearchCallback.mSearchResults) {
- assertNotNull(item.getMediaId());
- assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
- }
- assertNotNull(mSearchCallback.mSearchExtras);
- assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
- }
- }
-
- @Test
- @SmallTest
- public void testSendCustomAction() throws Exception {
- synchronized (mWaitLock) {
- CustomActionCallback callback = new CustomActionCallback();
- Bundle extras = new Bundle();
- extras.putString(TEST_KEY_1, TEST_VALUE_1);
- mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
- callback);
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return mMediaBrowserService.mCustomActionResult != null;
- }
- }.run();
- assertNotNull(mMediaBrowserService.mCustomActionResult);
- assertNotNull(mMediaBrowserService.mCustomActionExtras);
- assertEquals(TEST_VALUE_1,
- mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
-
- callback.reset();
- Bundle bundle1 = new Bundle();
- bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
- mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnProgressUpdateCalled);
- assertNotNull(callback.mExtras);
- assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
- assertNotNull(callback.mData);
- assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
-
- callback.reset();
- Bundle bundle2 = new Bundle();
- bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
- mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle2);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnProgressUpdateCalled);
- assertNotNull(callback.mExtras);
- assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
- assertNotNull(callback.mData);
- assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
-
- Bundle bundle3 = new Bundle();
- bundle3.putString(TEST_KEY_4, TEST_VALUE_4);
- callback.reset();
- mMediaBrowserService.mCustomActionResult.sendResult(bundle3);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnResultCalled);
- assertNotNull(callback.mExtras);
- assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
- assertNotNull(callback.mData);
- assertEquals(TEST_VALUE_4, callback.mData.getString(TEST_KEY_4));
- }
- }
-
- @Test
- @SmallTest
- public void testSendCustomActionWithDetachedError() throws Exception {
- synchronized (mWaitLock) {
- CustomActionCallback callback = new CustomActionCallback();
- Bundle extras = new Bundle();
- extras.putString(TEST_KEY_1, TEST_VALUE_1);
- mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
- callback);
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return mMediaBrowserService.mCustomActionResult != null;
- }
- }.run();
- assertNotNull(mMediaBrowserService.mCustomActionResult);
- assertNotNull(mMediaBrowserService.mCustomActionExtras);
- assertEquals(TEST_VALUE_1,
- mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
-
- callback.reset();
- Bundle bundle1 = new Bundle();
- bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
- mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnProgressUpdateCalled);
- assertNotNull(callback.mExtras);
- assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
- assertNotNull(callback.mData);
- assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
-
- callback.reset();
- Bundle bundle2 = new Bundle();
- bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
- mMediaBrowserService.mCustomActionResult.sendError(bundle2);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnErrorCalled);
- assertNotNull(callback.mExtras);
- assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
- assertNotNull(callback.mData);
- assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
- }
- }
-
- @Test
- @SmallTest
- public void testSendCustomActionWithNullCallback() throws Exception {
- Bundle extras = new Bundle();
- extras.putString(TEST_KEY_1, TEST_VALUE_1);
- mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras, null);
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return mMediaBrowserService.mCustomActionResult != null;
- }
- }.run();
- assertNotNull(mMediaBrowserService.mCustomActionResult);
- assertNotNull(mMediaBrowserService.mCustomActionExtras);
- assertEquals(TEST_VALUE_1, mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
- }
-
- @Test
- @SmallTest
- public void testSendCustomActionWithError() throws Exception {
- synchronized (mWaitLock) {
- CustomActionCallback callback = new CustomActionCallback();
- mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION_FOR_ERROR,
- null, callback);
- new PollingCheck(TIME_OUT_MS) {
- @Override
- protected boolean check() {
- return mMediaBrowserService.mCustomActionResult != null;
- }
- }.run();
- assertNotNull(mMediaBrowserService.mCustomActionResult);
- assertNull(mMediaBrowserService.mCustomActionExtras);
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertTrue(callback.mOnErrorCalled);
- }
- }
-
- @Test
- @SmallTest
- public void testBrowserRoot() {
- final String id = "test-id";
- final String key = "test-key";
- final String val = "test-val";
- final Bundle extras = new Bundle();
- extras.putString(key, val);
-
- MediaBrowserServiceCompat.BrowserRoot browserRoot =
- new MediaBrowserServiceCompat.BrowserRoot(id, extras);
- assertEquals(id, browserRoot.getRootId());
- assertEquals(val, browserRoot.getExtras().getString(key));
- }
-
- @Test
- @SmallTest
- public void testDelayedSetSessionToken() throws Exception {
- // This test has no meaning in API 21. The framework MediaBrowserService just connects to
- // the media browser without waiting setMediaSession() to be called.
- if (Build.VERSION.SDK_INT == 21) {
- return;
- }
- final ConnectionCallbackForDelayedMediaSession callback =
- new ConnectionCallbackForDelayedMediaSession();
-
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mMediaBrowserForDelayedMediaSession =
- new MediaBrowserCompat(getInstrumentation().getTargetContext(),
- TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION, callback, null);
- }
- });
-
- synchronized (mWaitLock) {
- mMediaBrowserForDelayedMediaSession.connect();
- mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
- assertEquals(0, callback.mConnectedCount);
-
- StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance.callSetSessionToken();
- mWaitLock.wait(TIME_OUT_MS);
- assertEquals(1, callback.mConnectedCount);
-
- if (Build.VERSION.SDK_INT >= 21) {
- assertNotNull(
- mMediaBrowserForDelayedMediaSession.getSessionToken().getExtraBinder());
- }
- }
- }
-
- private void assertRootHints(MediaItem item) {
- Bundle rootHints = item.getDescription().getExtras();
- assertNotNull(rootHints);
- assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
- rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT));
- assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE),
- rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE));
- assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED),
- rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED));
- }
-
- private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
- @Override
- public void onConnected() {
- synchronized (mWaitLock) {
- mMediaBrowserService = StubMediaBrowserServiceCompat.sInstance;
- mWaitLock.notify();
- }
- }
- }
-
- private class SubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
- boolean mOnChildrenLoaded;
- boolean mOnChildrenLoadedWithOptions;
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children) {
- synchronized (mWaitLock) {
- mOnChildrenLoaded = true;
- if (children != null) {
- for (MediaItem item : children) {
- assertRootHints(item);
- }
- }
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
- synchronized (mWaitLock) {
- mOnChildrenLoadedWithOptions = true;
- if (children != null) {
- for (MediaItem item : children) {
- assertRootHints(item);
- }
- }
- mWaitLock.notify();
- }
- }
-
- public void reset() {
- mOnChildrenLoaded = false;
- mOnChildrenLoadedWithOptions = false;
- }
- }
-
- private class ItemCallback extends MediaBrowserCompat.ItemCallback {
- boolean mOnItemLoaded;
-
- @Override
- public void onItemLoaded(MediaItem item) {
- synchronized (mWaitLock) {
- mOnItemLoaded = true;
- assertRootHints(item);
- mWaitLock.notify();
- }
- }
-
- public void reset() {
- mOnItemLoaded = false;
- }
- }
-
- private class SearchCallback extends MediaBrowserCompat.SearchCallback {
- boolean mOnSearchResult;
- Bundle mSearchExtras;
- List<MediaItem> mSearchResults;
-
- @Override
- public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
- synchronized (mWaitLock) {
- mOnSearchResult = true;
- mSearchResults = items;
- mSearchExtras = extras;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String query, Bundle extras) {
- synchronized (mWaitLock) {
- mOnSearchResult = true;
- mSearchResults = null;
- mSearchExtras = extras;
- mWaitLock.notify();
- }
- }
-
- public void reset() {
- mOnSearchResult = false;
- mSearchExtras = null;
- mSearchResults = null;
- }
- }
-
- private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
- String mAction;
- Bundle mExtras;
- Bundle mData;
- boolean mOnProgressUpdateCalled;
- boolean mOnResultCalled;
- boolean mOnErrorCalled;
-
- @Override
- public void onProgressUpdate(String action, Bundle extras, Bundle data) {
- synchronized (mWaitLock) {
- mOnProgressUpdateCalled = true;
- mAction = action;
- mExtras = extras;
- mData = data;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onResult(String action, Bundle extras, Bundle resultData) {
- synchronized (mWaitLock) {
- mOnResultCalled = true;
- mAction = action;
- mExtras = extras;
- mData = resultData;
- mWaitLock.notify();
- }
- }
-
- @Override
- public void onError(String action, Bundle extras, Bundle data) {
- synchronized (mWaitLock) {
- mOnErrorCalled = true;
- mAction = action;
- mExtras = extras;
- mData = data;
- mWaitLock.notify();
- }
- }
-
- public void reset() {
- mOnResultCalled = false;
- mOnProgressUpdateCalled = false;
- mOnErrorCalled = false;
- mAction = null;
- mExtras = null;
- mData = null;
- }
- }
-
- private class ConnectionCallbackForDelayedMediaSession extends
- MediaBrowserCompat.ConnectionCallback {
- private int mConnectedCount = 0;
-
- @Override
- public void onConnected() {
- synchronized (mWaitLock) {
- mConnectedCount++;
- mWaitLock.notify();
- }
- }
- }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
deleted file mode 100644
index c817dce..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import junit.framework.Assert;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
- static final String EXTRAS_KEY = "test_extras_key";
- static final String EXTRAS_VALUE = "test_extras_value";
-
- static final String MEDIA_ID = "test_media_id";
- static final String MEDIA_ID_INVALID = "test_media_id_invalid";
- static final String MEDIA_ID_ROOT = "test_media_id_root";
- static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed";
- static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED =
- "test_media_id_on_load_item_not_implemented";
-
- static final String[] MEDIA_ID_CHILDREN = new String[]{
- "test_media_id_children_0", "test_media_id_children_1",
- "test_media_id_children_2", "test_media_id_children_3",
- MEDIA_ID_CHILDREN_DELAYED
- };
-
- static final String SEARCH_QUERY = "children_2";
- static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
- static final String SEARCH_QUERY_FOR_ERROR = "query for error";
-
- static final String CUSTOM_ACTION = "CUSTOM_ACTION";
- static final String CUSTOM_ACTION_FOR_ERROR = "CUSTOM_ACTION_FOR_ERROR";
-
- static StubMediaBrowserServiceCompat sInstance;
-
- /* package private */ static MediaSessionCompat sSession;
- private Bundle mExtras;
- private Result<List<MediaItem>> mPendingLoadChildrenResult;
- private Result<MediaItem> mPendingLoadItemResult;
- private Bundle mPendingRootHints;
-
- /* package private */ Bundle mCustomActionExtras;
- /* package private */ Result<Bundle> mCustomActionResult;
-
- @Override
- public void onCreate() {
- super.onCreate();
- sInstance = this;
- sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
- setSessionToken(sSession.getSessionToken());
- }
-
- @Override
- public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
- mExtras = new Bundle();
- mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
- return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
- }
-
- @Override
- public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
- List<MediaItem> mediaItems = new ArrayList<>();
- if (MEDIA_ID_ROOT.equals(parentMediaId)) {
- Bundle rootHints = getBrowserRootHints();
- for (String id : MEDIA_ID_CHILDREN) {
- mediaItems.add(createMediaItem(id));
- }
- result.sendResult(mediaItems);
- } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
- Assert.assertNull(mPendingLoadChildrenResult);
- mPendingLoadChildrenResult = result;
- mPendingRootHints = getBrowserRootHints();
- result.detach();
- } else if (MEDIA_ID_INVALID.equals(parentMediaId)) {
- result.sendResult(null);
- }
- }
-
- @Override
- public void onLoadItem(String itemId, Result<MediaItem> result) {
- if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
- mPendingLoadItemResult = result;
- mPendingRootHints = getBrowserRootHints();
- result.detach();
- return;
- }
-
- if (MEDIA_ID_INVALID.equals(itemId)) {
- result.sendResult(null);
- return;
- }
-
- for (String id : MEDIA_ID_CHILDREN) {
- if (id.equals(itemId)) {
- result.sendResult(createMediaItem(id));
- return;
- }
- }
-
- // Test the case where onLoadItem is not implemented.
- super.onLoadItem(itemId, result);
- }
-
- @Override
- public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
- if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
- result.sendResult(Collections.<MediaItem>emptyList());
- } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
- result.sendResult(null);
- } else if (SEARCH_QUERY.equals(query)) {
- List<MediaItem> items = new ArrayList<>();
- for (String id : MEDIA_ID_CHILDREN) {
- if (id.contains(query)) {
- items.add(createMediaItem(id));
- }
- }
- result.sendResult(items);
- }
- }
-
- @Override
- public void onCustomAction(String action, Bundle extras,
- Result<Bundle> result) {
- mCustomActionResult = result;
- mCustomActionExtras = extras;
- if (CUSTOM_ACTION_FOR_ERROR.equals(action)) {
- result.sendError(null);
- } else if (CUSTOM_ACTION.equals(action)) {
- result.detach();
- }
- }
-
- public void sendDelayedNotifyChildrenChanged() {
- if (mPendingLoadChildrenResult != null) {
- mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
- mPendingRootHints = null;
- mPendingLoadChildrenResult = null;
- }
- }
-
- public void sendDelayedItemLoaded() {
- if (mPendingLoadItemResult != null) {
- mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
- MediaItem.FLAG_BROWSABLE));
- mPendingRootHints = null;
- mPendingLoadItemResult = null;
- }
- }
-
- private MediaItem createMediaItem(String id) {
- return new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(id).setExtras(getBrowserRootHints()).build(),
- MediaItem.FLAG_BROWSABLE);
- }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
deleted file mode 100644
index e93c940..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import java.util.List;
-
-/**
- * Stub implementation of {@link MediaBrowserServiceCompat}.
- * This implementation does not call
- * {@link MediaBrowserServiceCompat#setSessionToken(MediaSessionCompat.Token)} in its
- * {@link android.app.Service#onCreate}.
- */
-public class StubMediaBrowserServiceCompatWithDelayedMediaSession extends
- MediaBrowserServiceCompat {
-
- static StubMediaBrowserServiceCompatWithDelayedMediaSession sInstance;
- private MediaSessionCompat mSession;
-
- @Override
- public void onCreate() {
- super.onCreate();
- sInstance = this;
- mSession = new MediaSessionCompat(
- this, "StubMediaBrowserServiceCompatWithDelayedMediaSession");
- }
-
- @Nullable
- @Override
- public BrowserRoot onGetRoot(@NonNull String clientPackageName,
- int clientUid, @Nullable Bundle rootHints) {
- return new BrowserRoot("StubRootId", null);
- }
-
- @Override
- public void onLoadChildren(@NonNull String parentId,
- @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
- result.detach();
- }
-
- void callSetSessionToken() {
- setSessionToken(mSession.getSessionToken());
- }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
deleted file mode 100644
index 8e03ab2..0000000
--- a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.media;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.session.MediaSessionCompat;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
- */
-public class StubRemoteMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
- static final String EXTRAS_KEY = "test_extras_key";
- static final String EXTRAS_VALUE = "test_extras_value";
-
- static final String MEDIA_ID_ROOT = "test_media_id_root";
- static final String MEDIA_METADATA = "test_media_metadata";
-
- static final String[] MEDIA_ID_CHILDREN = new String[]{
- "test_media_id_children_0", "test_media_id_children_1",
- "test_media_id_children_2", "test_media_id_children_3"
- };
-
- private static MediaSessionCompat mSession;
- private Bundle mExtras;
-
- @Override
- public void onCreate() {
- super.onCreate();
- mSession = new MediaSessionCompat(this, "StubRemoteMediaBrowserServiceCompat");
- setSessionToken(mSession.getSessionToken());
- }
-
- @Override
- public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
- mExtras = new Bundle();
- mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
- return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
- }
-
- @Override
- public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
- List<MediaItem> mediaItems = new ArrayList<>();
- if (MEDIA_ID_ROOT.equals(parentMediaId)) {
- Bundle rootHints = getBrowserRootHints();
- for (String id : MEDIA_ID_CHILDREN) {
- mediaItems.add(createMediaItem(id));
- }
- result.sendResult(mediaItems);
- }
- }
-
- @Override
- public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result,
- final Bundle options) {
- MediaMetadataCompat metadata = options.getParcelable(MEDIA_METADATA);
- if (metadata == null) {
- super.onLoadChildren(parentMediaId, result, options);
- } else {
- List<MediaItem> mediaItems = new ArrayList<>();
- mediaItems.add(new MediaItem(metadata.getDescription(), MediaItem.FLAG_PLAYABLE));
- result.sendResult(mediaItems);
- }
- }
-
- private MediaItem createMediaItem(String id) {
- return new MediaItem(new MediaDescriptionCompat.Builder()
- .setMediaId(id).setExtras(getBrowserRootHints()).build(),
- MediaItem.FLAG_BROWSABLE);
- }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
index 53d7e47..b197a42 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
@@ -438,7 +438,7 @@
final long expectedUpdateTime = waitDuration + stateSetTime;
final long expectedPosition = (long) (TEST_PLAYBACK_SPEED * waitDuration) + TEST_POSITION;
- final double updateTimeTolerance = 30L;
+ final double updateTimeTolerance = 50L;
final double positionTolerance = updateTimeTolerance * TEST_PLAYBACK_SPEED;
PlaybackStateCompat stateOut = mSession.getController().getPlaybackState();
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
index 2cda242..9911c11 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -502,6 +502,29 @@
}
/**
+ * Tests {@link MediaSessionCompat#setCallback} with {@code null}. No callback will be called
+ * once {@code setCallback(null)} is done.
+ */
+ @Test
+ @SmallTest
+ public void testSetCallbackWithNull() throws Exception {
+ MediaSessionCallback sessionCallback = new MediaSessionCallback();
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mSession.setActive(true);
+ mSession.setCallback(sessionCallback, mHandler);
+
+ MediaControllerCompat controller = mSession.getController();
+ setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
+
+ sessionCallback.reset(1);
+ mSession.setCallback(null, mHandler);
+
+ controller.getTransportControls().pause();
+ assertFalse(sessionCallback.await(WAIT_TIME_MS));
+ assertFalse("Callback shouldn't be called.", sessionCallback.mOnPauseCalled);
+ }
+
+ /**
* Tests {@link MediaSessionCompat#setPlaybackToLocal} and
* {@link MediaSessionCompat#setPlaybackToRemote}.
*/
@@ -716,22 +739,6 @@
}
}
- @Test
- @SmallTest
- public void testSetNullCallback() throws Throwable {
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- try {
- MediaSessionCompat session = new MediaSessionCompat(getContext(), "TEST");
- session.setCallback(null);
- } catch (Exception e) {
- fail("Fail with an exception: " + e);
- }
- }
- });
- }
-
/**
* Tests {@link MediaSessionCompat.QueueItem}.
*/
diff --git a/media-compat-test-client/OWNERS b/media-compat/version-compat-tests/OWNERS
similarity index 100%
rename from media-compat-test-client/OWNERS
rename to media-compat/version-compat-tests/OWNERS
diff --git a/media-compat/version-compat-tests/current/client/AndroidManifest.xml b/media-compat/version-compat-tests/current/client/AndroidManifest.xml
new file mode 100644
index 0000000..9724d2b
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="android.support.mediacompat.client"/>
diff --git a/media-compat/version-compat-tests/current/client/build.gradle b/media-compat/version-compat-tests/current/client/build.gradle
new file mode 100644
index 0000000..d6f0e7d
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ androidTestImplementation project(':support-media-compat')
+ androidTestImplementation project(':support-media-compat-test-lib')
+
+ androidTestImplementation(libs.test_runner)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+}
+
+supportLibrary {
+ legacySourceLocation = true
+}
\ No newline at end of file
diff --git a/media-compat/version-compat-tests/current/client/lint-baseline.xml b/media-compat/version-compat-tests/current/client/lint-baseline.xml
new file mode 100644
index 0000000..ed7ade1
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/lint-baseline.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml b/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml
new file mode 100644
index 0000000..afe1865
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.mediacompat.client.test">
+ <application android:supportsRtl="true">
+ <receiver android:name="android.support.mediacompat.client.ClientBroadcastReceiver">
+ <intent-filter>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_CONTROLLER_METHOD"/>
+ <action android:name="android.support.mediacompat.service.action.CALL_TRANSPORT_CONTROLS_METHOD"/>
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/media-compat/version-compat-tests/current/client/tests/NO_DOCS b/media-compat/version-compat-tests/current/client/tests/NO_DOCS
new file mode 100644
index 0000000..61c9b1a
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
new file mode 100644
index 0000000..3166e55
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_MEDIA_CONTROLLER_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_TRANSPORT_CONTROLS_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_SESSION_TOKEN;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+public class ClientBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle extras = intent.getExtras();
+ MediaControllerCompat controller;
+ try {
+ controller = new MediaControllerCompat(context,
+ (MediaSessionCompat.Token) extras.getParcelable(KEY_SESSION_TOKEN));
+ } catch (RemoteException ex) {
+ // Do nothing.
+ return;
+ }
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ if (ACTION_CALL_MEDIA_CONTROLLER_METHOD.equals(intent.getAction()) && extras != null) {
+ Bundle arguments;
+ switch (method) {
+ case SEND_COMMAND:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controller.sendCommand(
+ arguments.getString("command"),
+ arguments.getBundle("extras"),
+ new ResultReceiver(null));
+ break;
+ case ADD_QUEUE_ITEM:
+ controller.addQueueItem(
+ (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case ADD_QUEUE_ITEM_WITH_INDEX:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controller.addQueueItem(
+ (MediaDescriptionCompat) arguments.getParcelable("description"),
+ arguments.getInt("index"));
+ break;
+ case REMOVE_QUEUE_ITEM:
+ controller.removeQueueItem(
+ (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ }
+ } else if (ACTION_CALL_TRANSPORT_CONTROLS_METHOD.equals(intent.getAction())
+ && extras != null) {
+ TransportControls controls = controller.getTransportControls();
+ Bundle arguments;
+ switch (method) {
+ case PLAY:
+ controls.play();
+ break;
+ case PAUSE:
+ controls.pause();
+ break;
+ case STOP:
+ controls.stop();
+ break;
+ case FAST_FORWARD:
+ controls.fastForward();
+ break;
+ case REWIND:
+ controls.rewind();
+ break;
+ case SKIP_TO_PREVIOUS:
+ controls.skipToPrevious();
+ break;
+ case SKIP_TO_NEXT:
+ controls.skipToNext();
+ break;
+ case SEEK_TO:
+ controls.seekTo(extras.getLong(KEY_ARGUMENT));
+ break;
+ case SET_RATING:
+ controls.setRating((RatingCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case PLAY_FROM_MEDIA_ID:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromMediaId(
+ arguments.getString("mediaId"),
+ arguments.getBundle("extras"));
+ break;
+ case PLAY_FROM_SEARCH:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromSearch(
+ arguments.getString("query"),
+ arguments.getBundle("extras"));
+ break;
+ case PLAY_FROM_URI:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromUri(
+ (Uri) arguments.getParcelable("uri"),
+ arguments.getBundle("extras"));
+ break;
+ case SEND_CUSTOM_ACTION:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.sendCustomAction(
+ arguments.getString("action"),
+ arguments.getBundle("extras"));
+ break;
+ case SEND_CUSTOM_ACTION_PARCELABLE:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.sendCustomAction(
+ (PlaybackStateCompat.CustomAction)
+ arguments.getParcelable("action"),
+ arguments.getBundle("extras"));
+ break;
+ case SKIP_TO_QUEUE_ITEM:
+ controls.skipToQueueItem(extras.getLong(KEY_ARGUMENT));
+ break;
+ case PREPARE:
+ controls.prepare();
+ break;
+ case PREPARE_FROM_MEDIA_ID:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromMediaId(
+ arguments.getString("mediaId"),
+ arguments.getBundle("extras"));
+ break;
+ case PREPARE_FROM_SEARCH:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromSearch(
+ arguments.getString("query"),
+ arguments.getBundle("extras"));
+ break;
+ case PREPARE_FROM_URI:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromUri(
+ (Uri) arguments.getParcelable("uri"),
+ arguments.getBundle("extras"));
+ break;
+ case SET_CAPTIONING_ENABLED:
+ controls.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case SET_REPEAT_MODE:
+ controls.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_SHUFFLE_MODE:
+ controls.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
new file mode 100644
index 0000000..0755e26
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INCLUDE_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_4;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_4;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.VersionConstants.VERSION_TOT;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaBrowserServiceMethod;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.content.ComponentName;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserCompatTest {
+
+ private static final String TAG = "MediaBrowserCompatTest";
+
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 300L;
+
+ /**
+ * To check {@link MediaBrowserCompat#unsubscribe} works properly,
+ * we notify to the browser after the unsubscription that the media items have changed.
+ * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
+ *
+ * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
+ * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
+ * 50ms.
+ * So we make the thread sleep for 100ms to properly check that the callback is not called.
+ */
+ private static final long SLEEP_MS = 100L;
+ private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+ private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
+ new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service"
+ + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+ private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
+ "invalid.package", "invalid.ServiceClassName");
+
+ private String mServiceVersion;
+ private MediaBrowserCompat mMediaBrowser;
+ private StubConnectionCallback mConnectionCallback;
+ private StubSubscriptionCallback mSubscriptionCallback;
+ private StubItemCallback mItemCallback;
+ private StubSearchCallback mSearchCallback;
+ private CustomActionCallback mCustomActionCallback;
+ private Bundle mRootHints;
+
+ @Before
+ public void setUp() {
+ // The version of the service app is provided through the instrumentation arguments.
+ mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+ Log.d(TAG, "Service app version: " + mServiceVersion);
+
+ mConnectionCallback = new StubConnectionCallback();
+ mSubscriptionCallback = new StubSubscriptionCallback();
+ mItemCallback = new StubItemCallback();
+ mSearchCallback = new StubSearchCallback();
+ mCustomActionCallback = new CustomActionCallback();
+
+ mRootHints = new Bundle();
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() {
+ if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+ mMediaBrowser.disconnect();
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testBrowserRoot() {
+ final String id = "test-id";
+ final String key = "test-key";
+ final String val = "test-val";
+ final Bundle extras = new Bundle();
+ extras.putString(key, val);
+
+ MediaBrowserServiceCompat.BrowserRoot browserRoot =
+ new MediaBrowserServiceCompat.BrowserRoot(id, extras);
+ assertEquals(id, browserRoot.getRootId());
+ assertEquals(val, browserRoot.getExtras().getString(key));
+ }
+
+ @Test
+ @SmallTest
+ public void testMediaBrowser() throws Exception {
+ assertFalse(mMediaBrowser.isConnected());
+
+ connectMediaBrowserService();
+ assertTrue(mMediaBrowser.isConnected());
+
+ assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+ assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+ assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
+
+ mMediaBrowser.disconnect();
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return !mMediaBrowser.isConnected();
+ }
+ }.run();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetServiceComponentBeforeConnection() {
+ try {
+ ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
+ fail();
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectionFailed() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_INVALID_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ }
+ assertEquals(1, mConnectionCallback.mConnectionFailedCount);
+ assertEquals(0, mConnectionCallback.mConnectedCount);
+ assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectTwice() throws Exception {
+ connectMediaBrowserService();
+ try {
+ mMediaBrowser.connect();
+ fail();
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testReconnection() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser.connect();
+ // Reconnect before the first connection was established.
+ mMediaBrowser.disconnect();
+ mMediaBrowser.connect();
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(1, mConnectionCallback.mConnectedCount);
+ }
+
+ // Test subscribe.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+
+ synchronized (mItemCallback.mWaitLock) {
+ // Test getItem.
+ mItemCallback.reset();
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+
+ // Reconnect after connection was established.
+ mMediaBrowser.disconnect();
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ // Test getItem.
+ mItemCallback.reset();
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testConnectionCallbackNotCalledAfterDisconnect() {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser.connect();
+ mMediaBrowser.disconnect();
+ mConnectionCallback.reset();
+ }
+ });
+
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+ assertEquals(0, mConnectionCallback.mConnectedCount);
+ assertEquals(0, mConnectionCallback.mConnectionFailedCount);
+ assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribe() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+ assertEquals(MEDIA_ID_CHILDREN.length, mSubscriptionCallback.mLastChildMediaItems.size());
+ for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
+ assertEquals(MEDIA_ID_CHILDREN[i],
+ mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+ }
+
+ // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+ mSubscriptionCallback.reset(1);
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+
+ // Test unsubscribe.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ // onChildrenLoaded should not be called.
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribeWithOptions() throws Exception {
+ connectMediaBrowserService();
+ final int pageSize = 3;
+ final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+
+ for (int page = 0; page <= lastPage; ++page) {
+ mSubscriptionCallback.reset(1);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+ if (page != lastPage) {
+ assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
+ } else {
+ assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
+ mSubscriptionCallback.mLastChildMediaItems.size());
+ }
+ // Check whether all the items in the current page are loaded.
+ for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
+ assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
+ mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+ }
+
+ // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+ mSubscriptionCallback.reset(page + 1);
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(page + 1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Test unsubscribe with callback argument.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+ // onChildrenLoaded should not be called.
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @SmallTest
+ public void testSubscribeWithOptionsIncludingCompatParcelables() throws Exception {
+ if (Build.VERSION.SDK_INT >= 26 && !VERSION_TOT.equals(mServiceVersion)) {
+ // This test will fail on API 26 or newer APIs if the service application uses
+ // support library v27.0.1 or lower versions.
+ return;
+ }
+ connectMediaBrowserService();
+
+ final String mediaId = "1000";
+ final RatingCompat percentageRating = RatingCompat.newPercentageRating(0.5f);
+ final RatingCompat starRating =
+ RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 4.0f);
+ MediaMetadataCompat mediaMetadataCompat = new MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title")
+ .putRating(MediaMetadataCompat.METADATA_KEY_RATING, percentageRating)
+ .putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING, starRating)
+ .build();
+ Bundle options = new Bundle();
+ options.putParcelable(MEDIA_METADATA, mediaMetadataCompat);
+
+ // Remote MediaBrowserService will create a media item with the given MediaMetadataCompat.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_INCLUDE_METADATA, options, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
+ assertEquals(1, mSubscriptionCallback.mLastChildMediaItems.size());
+ assertEquals(mediaId, mSubscriptionCallback.mLastChildMediaItems.get(0).getMediaId());
+
+ MediaMetadataCompat metadataOut = mSubscriptionCallback.mLastOptions
+ .getParcelable(MEDIA_METADATA);
+ assertEquals(mediaId, metadataOut.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID));
+ assertEquals("title", metadataOut.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
+ assertRatingEquals(percentageRating,
+ metadataOut.getRating(MediaMetadataCompat.METADATA_KEY_RATING));
+ assertRatingEquals(starRating,
+ metadataOut.getRating(MediaMetadataCompat.METADATA_KEY_USER_RATING));
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribeDelayedItems() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_CHILDREN_DELAYED, mSubscriptionCallback);
+ mSubscriptionCallback.await(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+
+ callMediaBrowserServiceMethod(
+ SEND_DELAYED_NOTIFY_CHILDREN_CHANGED, MEDIA_ID_CHILDREN_DELAYED, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @SmallTest
+ public void testSubscribeInvalidItem() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+ }
+
+ @Test
+ @SmallTest
+ public void testSubscribeInvalidItemWithOptions() throws Exception {
+ connectMediaBrowserService();
+
+ final int pageSize = 5;
+ final int page = 2;
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+ assertNotNull(mSubscriptionCallback.mLastOptions);
+ assertEquals(page,
+ mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
+ assertEquals(pageSize,
+ mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
+ }
+
+ @Test
+ @MediumTest
+ public void testUnsubscribeForMultipleSubscriptions() throws Exception {
+ connectMediaBrowserService();
+ final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+ final int pageSize = 1;
+
+ // Subscribe four pages, one item per page.
+ for (int page = 0; page < 4; page++) {
+ final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+ subscriptionCallbacks.add(callback);
+
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ callback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+ callback.await(TIME_OUT_MS);
+
+ // Each onChildrenLoaded() must be called.
+ assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Reset callbacks and unsubscribe.
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ callback.reset(1);
+ }
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+
+ // onChildrenLoaded should not be called.
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ assertEquals(0, callback.mChildrenLoadedWithOptionCount);
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
+ connectMediaBrowserService();
+ final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+ final int pageSize = 1;
+
+ // Subscribe four pages, one item per page.
+ for (int page = 0; page < 4; page++) {
+ final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+ subscriptionCallbacks.add(callback);
+
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ callback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+ callback.await(TIME_OUT_MS);
+
+ // Each onChildrenLoaded() must be called.
+ assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Unsubscribe existing subscriptions one-by-one.
+ final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
+ for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
+ // Reset callbacks
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ callback.reset(1);
+ }
+
+ // Remove one subscription
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
+ subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
+
+ // Make StubMediaBrowserServiceCompat notify that the children are changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+
+ // Only the remaining subscriptionCallbacks should be called.
+ for (int j = 0; j < 4; j++) {
+ int childrenLoadedWithOptionsCount = subscriptionCallbacks
+ .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
+ if (j <= i) {
+ assertEquals(0, childrenLoadedWithOptionsCount);
+ } else {
+ assertEquals(1, childrenLoadedWithOptionsCount);
+ }
+ }
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItem() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNotNull(mItemCallback.mLastMediaItem);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testGetItemDelayed() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
+ mItemCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertNull(mItemCallback.mLastMediaItem);
+
+ mItemCallback.reset();
+ callMediaBrowserServiceMethod(SEND_DELAYED_ITEM_LOADED, new Bundle(), getContext());
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNotNull(mItemCallback.mLastMediaItem);
+ assertEquals(MEDIA_ID_CHILDREN_DELAYED, mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
+ connectMediaBrowserService();
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItemWhenMediaIdIsInvalid() throws Exception {
+ mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
+
+ connectMediaBrowserService();
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNull(mItemCallback.mLastMediaItem);
+ assertNull(mItemCallback.mLastErrorId);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSearch() throws Exception {
+ connectMediaBrowserService();
+
+ final String key = "test-key";
+ final String val = "test-val";
+
+ synchronized (mSearchCallback.mWaitLock) {
+ mSearchCallback.reset();
+ mMediaBrowser.search(SEARCH_QUERY_FOR_NO_RESULT, null, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertTrue(mSearchCallback.mSearchResults != null
+ && mSearchCallback.mSearchResults.size() == 0);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ mMediaBrowser.search(SEARCH_QUERY_FOR_ERROR, null, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNull(mSearchCallback.mSearchResults);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ Bundle extras = new Bundle();
+ extras.putString(key, val);
+ mMediaBrowser.search(SEARCH_QUERY, extras, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNotNull(mSearchCallback.mSearchResults);
+ for (MediaItem item : mSearchCallback.mSearchResults) {
+ assertNotNull(item.getMediaId());
+ assertTrue(item.getMediaId().contains(SEARCH_QUERY));
+ }
+ assertNotNull(mSearchCallback.mSearchExtras);
+ assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCustomAction() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(
+ CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ mCustomActionCallback.reset();
+ Bundle data1 = new Bundle();
+ data1.putString(TEST_KEY_2, TEST_VALUE_2);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data1, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+ mCustomActionCallback.reset();
+ Bundle data2 = new Bundle();
+ data2.putString(TEST_KEY_3, TEST_VALUE_3);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data2, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+
+ Bundle resultData = new Bundle();
+ resultData.putString(TEST_KEY_4, TEST_VALUE_4);
+ mCustomActionCallback.reset();
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, resultData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnResultCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_4, mCustomActionCallback.mData.getString(TEST_KEY_4));
+ }
+ }
+
+
+ @Test
+ @MediumTest
+ public void testSendCustomActionWithDetachedError() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(
+ CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ mCustomActionCallback.reset();
+ Bundle progressUpdateData = new Bundle();
+ progressUpdateData.putString(TEST_KEY_2, TEST_VALUE_2);
+ callMediaBrowserServiceMethod(
+ CUSTOM_ACTION_SEND_PROGRESS_UPDATE, progressUpdateData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+ mCustomActionCallback.reset();
+ Bundle errorData = new Bundle();
+ errorData.putString(TEST_KEY_3, TEST_VALUE_3);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_ERROR, errorData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnErrorCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testSendCustomActionWithNullCallback() throws Exception {
+ connectMediaBrowserService();
+
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+ // Wait some time so that the service can get a result receiver for the custom action.
+ Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ // These calls should not make any exceptions.
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
+ getContext());
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, new Bundle(), getContext());
+ Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCustomActionWithError() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ mMediaBrowser.sendCustomAction(CUSTOM_ACTION_FOR_ERROR, null, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnErrorCalled);
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testDelayedSetSessionToken() throws Exception {
+ // This test has no meaning in API 21. The framework MediaBrowserService just connects to
+ // the media browser without waiting setMediaSession() to be called.
+ if (Build.VERSION.SDK_INT == 21) {
+ return;
+ }
+ final ConnectionCallbackForDelayedMediaSession callback =
+ new ConnectionCallbackForDelayedMediaSession();
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(
+ getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION,
+ callback,
+ null);
+ }
+ });
+
+ synchronized (callback.mWaitLock) {
+ mMediaBrowser.connect();
+ callback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertEquals(0, callback.mConnectedCount);
+
+ callMediaBrowserServiceMethod(SET_SESSION_TOKEN, new Bundle(), getContext());
+ callback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(1, callback.mConnectedCount);
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ assertNotNull(mMediaBrowser.getSessionToken().getExtraBinder());
+ }
+ }
+ }
+
+ private void connectMediaBrowserService() throws Exception {
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ if (!mMediaBrowser.isConnected()) {
+ fail("Browser failed to connect!");
+ }
+ }
+ }
+
+ private void assertRatingEquals(RatingCompat expected, RatingCompat observed) {
+ if (expected == null || observed == null) {
+ assertSame(expected, observed);
+ }
+ assertEquals(expected.getRatingStyle(), observed.getRatingStyle());
+
+ if (expected.getRatingStyle() == RatingCompat.RATING_PERCENTAGE) {
+ assertEquals(expected.getPercentRating(), observed.getPercentRating());
+ } else if (expected.getRatingStyle() == RatingCompat.RATING_5_STARS) {
+ assertEquals(expected.getStarRating(), observed.getStarRating());
+ } else {
+ // Currently, we use only star and percentage rating.
+ fail("Rating style should be either percentage rating or star rating.");
+ }
+ }
+
+ private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+ volatile int mConnectedCount;
+ volatile int mConnectionFailedCount;
+ volatile int mConnectionSuspendedCount;
+
+ public void reset() {
+ mConnectedCount = 0;
+ mConnectionFailedCount = 0;
+ mConnectionSuspendedCount = 0;
+ }
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mConnectedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ synchronized (mWaitLock) {
+ mConnectionFailedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ synchronized (mWaitLock) {
+ mConnectionSuspendedCount++;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+ private CountDownLatch mLatch;
+ private volatile int mChildrenLoadedCount;
+ private volatile int mChildrenLoadedWithOptionCount;
+ private volatile String mLastErrorId;
+ private volatile String mLastParentId;
+ private volatile Bundle mLastOptions;
+ private volatile List<MediaItem> mLastChildMediaItems;
+
+ public void reset(int count) {
+ mLatch = new CountDownLatch(count);
+ mChildrenLoadedCount = 0;
+ mChildrenLoadedWithOptionCount = 0;
+ mLastErrorId = null;
+ mLastParentId = null;
+ mLastOptions = null;
+ mLastChildMediaItems = null;
+ }
+
+ public boolean await(long timeoutMs) {
+ try {
+ return mLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
+ mChildrenLoadedCount++;
+ mLastParentId = parentId;
+ mLastChildMediaItems = children;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
+ @NonNull Bundle options) {
+ mChildrenLoadedWithOptionCount++;
+ mLastParentId = parentId;
+ mLastOptions = options;
+ mLastChildMediaItems = children;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull String id) {
+ mLastErrorId = id;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull String id, @NonNull Bundle options) {
+ mLastErrorId = id;
+ mLastOptions = options;
+ mLatch.countDown();
+ }
+ }
+
+ private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
+ final Object mWaitLock = new Object();
+ private volatile MediaItem mLastMediaItem;
+ private volatile String mLastErrorId;
+
+ public void reset() {
+ mLastMediaItem = null;
+ mLastErrorId = null;
+ }
+
+ @Override
+ public void onItemLoaded(MediaItem item) {
+ synchronized (mWaitLock) {
+ mLastMediaItem = item;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String id) {
+ synchronized (mWaitLock) {
+ mLastErrorId = id;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class StubSearchCallback extends MediaBrowserCompat.SearchCallback {
+ final Object mWaitLock = new Object();
+ boolean mOnSearchResult;
+ Bundle mSearchExtras;
+ List<MediaItem> mSearchResults;
+
+ @Override
+ public void onSearchResult(@NonNull String query, Bundle extras,
+ @NonNull List<MediaItem> items) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = items;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = null;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ public void reset() {
+ mOnSearchResult = false;
+ mSearchExtras = null;
+ mSearchResults = null;
+ }
+ }
+
+ private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
+ final Object mWaitLock = new Object();
+ String mAction;
+ Bundle mExtras;
+ Bundle mData;
+ boolean mOnProgressUpdateCalled;
+ boolean mOnResultCalled;
+ boolean mOnErrorCalled;
+
+ @Override
+ public void onProgressUpdate(String action, Bundle extras, Bundle data) {
+ synchronized (mWaitLock) {
+ mOnProgressUpdateCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = data;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onResult(String action, Bundle extras, Bundle resultData) {
+ synchronized (mWaitLock) {
+ mOnResultCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = resultData;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(String action, Bundle extras, Bundle data) {
+ synchronized (mWaitLock) {
+ mOnErrorCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = data;
+ mWaitLock.notify();
+ }
+ }
+
+ public void reset() {
+ mOnResultCalled = false;
+ mOnProgressUpdateCalled = false;
+ mOnErrorCalled = false;
+ mAction = null;
+ mExtras = null;
+ mData = null;
+ }
+ }
+
+ private class ConnectionCallbackForDelayedMediaSession extends
+ MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+ private int mConnectedCount = 0;
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mConnectedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
new file mode 100644
index 0000000..5dac1b6
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.media.AudioManager.STREAM_MUSIC;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ACTION;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_CURRENT_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_CODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_MSG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MAX_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaSessionMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaControllerCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatCallbackTest {
+
+ private static final String TAG = "MediaControllerCompatCallbackTest";
+
+ // The maximum time to wait for an operation, that is expected to happen.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+
+ private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Object mWaitLock = new Object();
+
+ private String mServiceVersion;
+
+ // MediaBrowserCompat object to get the session token.
+ private MediaBrowserCompat mMediaBrowser;
+ private ConnectionCallback mConnectionCallback = new ConnectionCallback();
+
+ private MediaSessionCompat.Token mSessionToken;
+ private MediaControllerCompat mController;
+ private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
+
+ @Before
+ public void setUp() throws Exception {
+ // The version of the service app is provided through the instrumentation arguments.
+ mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+ Log.d(TAG, "Service app version: " + mServiceVersion);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE, mConnectionCallback, new Bundle());
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ if (!mMediaBrowser.isConnected()) {
+ fail("Browser failed to connect!");
+ }
+ }
+ mSessionToken = mMediaBrowser.getSessionToken();
+ mController = new MediaControllerCompat(getTargetContext(), mSessionToken);
+ mController.registerCallback(mMediaControllerCallback, mHandler);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+ mMediaBrowser.disconnect();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setExtras}.
+ */
+ @Test
+ @SmallTest
+ public void testSetExtras() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ callMediaSessionMethod(SET_EXTRAS, extras, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnExtraChangedCalled);
+
+ assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+ assertBundleEquals(extras, mController.getExtras());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setFlags}.
+ */
+ @Test
+ @SmallTest
+ public void testSetFlags() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ callMediaSessionMethod(SET_FLAGS, TEST_FLAGS, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return TEST_FLAGS == mController.getFlags();
+ }
+ }.run();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setMetadata}.
+ */
+ @Test
+ @SmallTest
+ public void testSetMetadata() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ RatingCompat rating = RatingCompat.newHeartRating(true);
+ MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+ .putString(TEST_KEY, TEST_VALUE)
+ .putRating(METADATA_KEY_RATING, rating)
+ .build();
+
+ callMediaSessionMethod(SET_METADATA, metadata, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+ MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ metadataOut = mController.getMetadata();
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ assertNotNull(metadataOut.getRating(METADATA_KEY_RATING));
+ RatingCompat ratingOut = metadataOut.getRating(METADATA_KEY_RATING);
+ assertEquals(rating.getRatingStyle(), ratingOut.getRatingStyle());
+ assertEquals(rating.getPercentRating(), ratingOut.getPercentRating(), 0.0f);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setMetadata} with artwork bitmaps.
+ */
+ @Test
+ @SmallTest
+ public void testSetMetadataWithArtworks() throws Exception {
+ // TODO: Add test with a large bitmap.
+ // Using large bitmap makes other tests that are executed after this fail.
+ final Bitmap bitmapSmall = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+ .putString(TEST_KEY, TEST_VALUE)
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmapSmall)
+ .build();
+
+ callMediaSessionMethod(SET_METADATA, metadata, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+ MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ Bitmap bitmapSmallOut = metadataOut.getBitmap(MediaMetadataCompat.METADATA_KEY_ART);
+ assertNotNull(bitmapSmallOut);
+ assertEquals(bitmapSmall.getHeight(), bitmapSmallOut.getHeight());
+ assertEquals(bitmapSmall.getWidth(), bitmapSmallOut.getWidth());
+ assertEquals(bitmapSmall.getConfig(), bitmapSmallOut.getConfig());
+
+ bitmapSmallOut.recycle();
+ }
+ bitmapSmall.recycle();
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setPlaybackState}.
+ */
+ @Test
+ @SmallTest
+ public void testSetPlaybackState() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ PlaybackStateCompat state =
+ new PlaybackStateCompat.Builder()
+ .setActions(TEST_ACTION)
+ .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+ .build();
+
+ callMediaSessionMethod(SET_PLAYBACK_STATE, state, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnPlaybackStateChangedCalled);
+
+ PlaybackStateCompat stateOut = mMediaControllerCallback.mPlaybackState;
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+ assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+
+ stateOut = mController.getPlaybackState();
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+ assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setQueue} and {@link MediaSessionCompat#setQueueTitle}.
+ */
+ @Test
+ @SmallTest
+ public void testSetQueueAndSetQueueTitle() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ List<QueueItem> queue = new ArrayList<>();
+
+ MediaDescriptionCompat description1 =
+ new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_1).build();
+ MediaDescriptionCompat description2 =
+ new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_2).build();
+ QueueItem item1 = new MediaSessionCompat.QueueItem(description1, TEST_QUEUE_ID_1);
+ QueueItem item2 = new MediaSessionCompat.QueueItem(description2, TEST_QUEUE_ID_2);
+ queue.add(item1);
+ queue.add(item2);
+
+ callMediaSessionMethod(SET_QUEUE, queue, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+ callMediaSessionMethod(SET_QUEUE_TITLE, TEST_VALUE, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+ assertEquals(TEST_VALUE, mMediaControllerCallback.mTitle);
+ assertQueueEquals(queue, mMediaControllerCallback.mQueue);
+
+ assertEquals(TEST_VALUE, mController.getQueueTitle());
+ assertQueueEquals(queue, mController.getQueue());
+
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_QUEUE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+ callMediaSessionMethod(SET_QUEUE_TITLE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+ assertNull(mMediaControllerCallback.mTitle);
+ assertNull(mMediaControllerCallback.mQueue);
+ assertNull(mController.getQueueTitle());
+ assertNull(mController.getQueue());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setSessionActivity}.
+ */
+ @Test
+ @SmallTest
+ public void testSessionActivity() throws Exception {
+ synchronized (mWaitLock) {
+ Intent intent = new Intent("MEDIA_SESSION_ACTION");
+ final int requestCode = 555;
+ final PendingIntent pi =
+ PendingIntent.getActivity(getTargetContext(), requestCode, intent, 0);
+
+ callMediaSessionMethod(SET_SESSION_ACTIVITY, pi, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return pi.equals(mController.getSessionActivity());
+ }
+ }.run();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setCaptioningEnabled}.
+ */
+ @Test
+ @SmallTest
+ public void testSetCaptioningEnabled() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_CAPTIONING_ENABLED, true, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+ assertEquals(true, mMediaControllerCallback.mCaptioningEnabled);
+ assertEquals(true, mController.isCaptioningEnabled());
+
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_CAPTIONING_ENABLED, false, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+ assertEquals(false, mMediaControllerCallback.mCaptioningEnabled);
+ assertEquals(false, mController.isCaptioningEnabled());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setRepeatMode}.
+ */
+ @Test
+ @SmallTest
+ public void testSetRepeatMode() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ callMediaSessionMethod(SET_REPEAT_MODE, repeatMode, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnRepeatModeChangedCalled);
+ assertEquals(repeatMode, mMediaControllerCallback.mRepeatMode);
+ assertEquals(repeatMode, mController.getRepeatMode());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setShuffleMode}.
+ */
+ @Test
+ @SmallTest
+ public void testSetShuffleMode() throws Exception {
+ final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_SHUFFLE_MODE, shuffleMode, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnShuffleModeChangedCalled);
+ assertEquals(shuffleMode, mMediaControllerCallback.mShuffleMode);
+ assertEquals(shuffleMode, mController.getShuffleMode());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#sendSessionEvent}.
+ */
+ @Test
+ @SmallTest
+ public void testSendSessionEvent() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ Bundle arguments = new Bundle();
+ arguments.putString("event", TEST_SESSION_EVENT);
+
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ arguments.putBundle("extras", extras);
+ callMediaSessionMethod(SEND_SESSION_EVENT, arguments, getContext());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnSessionEventCalled);
+ assertEquals(TEST_SESSION_EVENT, mMediaControllerCallback.mEvent);
+ assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#release}.
+ */
+ @Test
+ @SmallTest
+ public void testRelease() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(RELEASE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnSessionDestroyedCalled);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
+ * {@link MediaSessionCompat#setPlaybackToRemote}.
+ */
+ @LargeTest
+ public void testPlaybackToLocalAndRemote() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ ParcelableVolumeInfo volumeInfo = new ParcelableVolumeInfo(
+ MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ STREAM_MUSIC,
+ VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ TEST_MAX_VOLUME,
+ TEST_CURRENT_VOLUME);
+
+ callMediaSessionMethod(SET_PLAYBACK_TO_REMOTE, volumeInfo, getContext());
+ MediaControllerCompat.PlaybackInfo info = null;
+ for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+ mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+ info = mMediaControllerCallback.mPlaybackInfo;
+ if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+ && info.getMaxVolume() == TEST_MAX_VOLUME
+ && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+ && info.getPlaybackType()
+ == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ break;
+ }
+ }
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ info.getVolumeControl());
+
+ info = mController.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+ // test setPlaybackToLocal
+ mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+ callMediaSessionMethod(SET_PLAYBACK_TO_LOCAL, AudioManager.STREAM_RING, getContext());
+
+ // In API 21 and 22, onAudioInfoChanged is not called.
+ if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) {
+ Thread.sleep(TIME_OUT_MS);
+ } else {
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+ }
+
+ info = mController.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ info.getPlaybackType());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetRatingType() {
+ assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+ RatingCompat.RATING_NONE, mController.getRatingType());
+
+ callMediaSessionMethod(SET_RATING_TYPE, RatingCompat.RATING_5_STARS, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return RatingCompat.RATING_5_STARS == mController.getRatingType();
+ }
+ }.run();
+ }
+
+ @Test
+ @SmallTest
+ public void testSessionReady() throws Exception {
+ if (android.os.Build.VERSION.SDK_INT < 21) {
+ return;
+ }
+
+ final MediaSessionCompat.Token tokenWithoutExtraBinder =
+ MediaSessionCompat.Token.fromToken(mSessionToken.getToken());
+
+ final MediaControllerCallback callback = new MediaControllerCallback();
+ synchronized (mWaitLock) {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ MediaControllerCompat controller = new MediaControllerCompat(
+ getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
+ controller.registerCallback(callback, new Handler());
+ assertFalse(controller.isSessionReady());
+ } catch (Exception e) {
+ fail();
+ }
+ }
+ });
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(callback.mOnSessionReadyCalled);
+ }
+ }
+
+ private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
+ if (expected == null || observed == null) {
+ assertTrue(expected == observed);
+ return;
+ }
+
+ assertEquals(expected.size(), observed.size());
+ for (int i = 0; i < expected.size(); i++) {
+ QueueItem expectedItem = expected.get(i);
+ QueueItem observedItem = observed.get(i);
+
+ assertEquals(expectedItem.getQueueId(), observedItem.getQueueId());
+ assertEquals(expectedItem.getDescription().getMediaId(),
+ observedItem.getDescription().getMediaId());
+ }
+ }
+
+ private class MediaControllerCallback extends MediaControllerCompat.Callback {
+ private volatile boolean mOnPlaybackStateChangedCalled;
+ private volatile boolean mOnMetadataChangedCalled;
+ private volatile boolean mOnQueueChangedCalled;
+ private volatile boolean mOnQueueTitleChangedCalled;
+ private volatile boolean mOnExtraChangedCalled;
+ private volatile boolean mOnAudioInfoChangedCalled;
+ private volatile boolean mOnSessionDestroyedCalled;
+ private volatile boolean mOnSessionEventCalled;
+ private volatile boolean mOnCaptioningEnabledChangedCalled;
+ private volatile boolean mOnRepeatModeChangedCalled;
+ private volatile boolean mOnShuffleModeChangedCalled;
+ private volatile boolean mOnSessionReadyCalled;
+
+ private volatile PlaybackStateCompat mPlaybackState;
+ private volatile MediaMetadataCompat mMediaMetadata;
+ private volatile List<QueueItem> mQueue;
+ private volatile CharSequence mTitle;
+ private volatile String mEvent;
+ private volatile Bundle mExtras;
+ private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+ private volatile boolean mCaptioningEnabled;
+ private volatile int mRepeatMode;
+ private volatile int mShuffleMode;
+
+ public void resetLocked() {
+ mOnPlaybackStateChangedCalled = false;
+ mOnMetadataChangedCalled = false;
+ mOnQueueChangedCalled = false;
+ mOnQueueTitleChangedCalled = false;
+ mOnExtraChangedCalled = false;
+ mOnAudioInfoChangedCalled = false;
+ mOnSessionDestroyedCalled = false;
+ mOnSessionEventCalled = false;
+ mOnRepeatModeChangedCalled = false;
+ mOnShuffleModeChangedCalled = false;
+
+ mPlaybackState = null;
+ mMediaMetadata = null;
+ mQueue = null;
+ mTitle = null;
+ mExtras = null;
+ mPlaybackInfo = null;
+ mCaptioningEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ synchronized (mWaitLock) {
+ mOnPlaybackStateChangedCalled = true;
+ mPlaybackState = state;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ synchronized (mWaitLock) {
+ mOnMetadataChangedCalled = true;
+ mMediaMetadata = metadata;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueChanged(List<QueueItem> queue) {
+ synchronized (mWaitLock) {
+ mOnQueueChangedCalled = true;
+ mQueue = queue;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) {
+ synchronized (mWaitLock) {
+ mOnQueueTitleChangedCalled = true;
+ mTitle = title;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnExtraChangedCalled = true;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+ synchronized (mWaitLock) {
+ mOnAudioInfoChangedCalled = true;
+ mPlaybackInfo = info;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ synchronized (mWaitLock) {
+ mOnSessionDestroyedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSessionEventCalled = true;
+ mEvent = event;
+ mExtras = (Bundle) extras.clone();
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCaptioningEnabledChanged(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnCaptioningEnabledChangedCalled = true;
+ mCaptioningEnabled = enabled;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnRepeatModeChangedCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onShuffleModeChanged(int shuffleMode) {
+ synchronized (mWaitLock) {
+ mOnShuffleModeChangedCalled = true;
+ mShuffleMode = shuffleMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionReady() {
+ synchronized (mWaitLock) {
+ mOnSessionReadyCalled = true;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/current/service/AndroidManifest.xml b/media-compat/version-compat-tests/current/service/AndroidManifest.xml
new file mode 100644
index 0000000..5e25a83
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="android.support.mediacompat.service"/>
diff --git a/media-compat/version-compat-tests/current/service/build.gradle b/media-compat/version-compat-tests/current/service/build.gradle
new file mode 100644
index 0000000..2cfa5ce
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ androidTestImplementation project(':support-media-compat')
+ androidTestImplementation project(':support-media-compat-test-lib')
+
+ androidTestImplementation(libs.test_runner)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+}
+
+supportLibrary {
+ legacySourceLocation = true
+}
diff --git a/media-compat/version-compat-tests/current/service/lint-baseline.xml b/media-compat/version-compat-tests/current/service/lint-baseline.xml
new file mode 100644
index 0000000..ed7ade1
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/lint-baseline.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml b/media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml
new file mode 100644
index 0000000..b47eecf
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.mediacompat.service.test">
+ <application>
+ <receiver android:name="android.support.mediacompat.service.ServiceBroadcastReceiver">
+ <intent-filter>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"/>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
+ <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompat">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
+
+ <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompatWithDelayedMediaSession">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/media-compat/version-compat-tests/current/service/tests/NO_DOCS b/media-compat/version-compat-tests/current/service/tests/NO_DOCS
new file mode 100644
index 0000000..61c9b1a
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
new file mode 100644
index 0000000..d36eba3
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_COMMAND;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_TAG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_CLIENT_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaControllerMethod;
+import static android.support.mediacompat.testlib.util.IntentUtil.callTransportControlsMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaSessionCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaSessionCompatCallbackTest {
+
+ private static final String TAG = "MediaSessionCompatCallbackTest";
+
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final float DELTA = 1e-4f;
+ private static final boolean ENABLED = true;
+
+ private final Object mWaitLock = new Object();
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private String mClientVersion;
+ private MediaSessionCompat mSession;
+ private MediaSessionCallback mCallback = new MediaSessionCallback();
+
+ @Before
+ public void setUp() throws Exception {
+ // The version of the client app is provided through the instrumentation arguments.
+ mClientVersion = getArguments().getString(KEY_CLIENT_VERSION, "");
+ Log.d(TAG, "Client app version: " + mClientVersion);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSessionCompat(getTargetContext(), TEST_SESSION_TAG);
+ mSession.setCallback(mCallback, mHandler);
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSession.release();
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCommand() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+
+ Bundle arguments = new Bundle();
+ arguments.putString("command", TEST_COMMAND);
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ arguments.putBundle("extras", extras);
+ callMediaControllerMethod(
+ SEND_COMMAND, arguments, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCommandCalled);
+ assertNotNull(mCallback.mCommandCallback);
+ assertEquals(TEST_COMMAND, mCallback.mCommand);
+ assertBundleEquals(extras, mCallback.mExtras);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testAddRemoveQueueItems() throws Exception {
+ final String mediaId1 = "media_id_1";
+ final String mediaTitle1 = "media_title_1";
+ MediaDescriptionCompat itemDescription1 = new MediaDescriptionCompat.Builder()
+ .setMediaId(mediaId1).setTitle(mediaTitle1).build();
+
+ final String mediaId2 = "media_id_2";
+ final String mediaTitle2 = "media_title_2";
+ MediaDescriptionCompat itemDescription2 = new MediaDescriptionCompat.Builder()
+ .setMediaId(mediaId2).setTitle(mediaTitle2).build();
+
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ callMediaControllerMethod(
+ ADD_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnAddQueueItemCalled);
+ assertEquals(-1, mCallback.mQueueIndex);
+ assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+
+ mCallback.reset();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable("description", itemDescription2);
+ arguments.putInt("index", 0);
+ callMediaControllerMethod(
+ ADD_QUEUE_ITEM_WITH_INDEX, arguments, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnAddQueueItemAtCalled);
+ assertEquals(0, mCallback.mQueueIndex);
+ assertEquals(mediaId2, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle2, mCallback.mQueueDescription.getTitle());
+
+ mCallback.reset();
+ callMediaControllerMethod(
+ REMOVE_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRemoveQueueItemCalled);
+ assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testTransportControlsAndMediaSessionCallback() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ callTransportControlsMethod(PLAY, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(PAUSE, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPauseCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(STOP, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnStopCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ FAST_FORWARD, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnFastForwardCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(REWIND, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRewindCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SKIP_TO_PREVIOUS, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SKIP_TO_NEXT, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToNextCalled);
+
+ mCallback.reset();
+ final long seekPosition = 1000;
+ callTransportControlsMethod(
+ SEEK_TO, seekPosition, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSeekToCalled);
+ assertEquals(seekPosition, mCallback.mSeekPosition);
+
+ mCallback.reset();
+ final RatingCompat rating =
+ RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+ callTransportControlsMethod(
+ SET_RATING, rating, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRatingCalled);
+ assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+ assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+ mCallback.reset();
+ final String mediaId = "test-media-id";
+ final Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ Bundle arguments = new Bundle();
+ arguments.putString("mediaId", mediaId);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final String query = "test-query";
+ arguments = new Bundle();
+ arguments.putString("query", query);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final Uri uri = Uri.parse("content://test/popcorn.mod");
+ arguments = new Bundle();
+ arguments.putParcelable("uri", uri);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final String action = "test-action";
+ arguments = new Bundle();
+ arguments.putString("action", action);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ SEND_CUSTOM_ACTION, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ mCallback.mOnCustomActionCalled = false;
+ final PlaybackStateCompat.CustomAction customAction =
+ new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+ .setExtras(extras)
+ .build();
+ arguments = new Bundle();
+ arguments.putParcelable("action", customAction);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ SEND_CUSTOM_ACTION_PARCELABLE,
+ arguments,
+ getContext(),
+ mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final long queueItemId = 1000;
+ callTransportControlsMethod(
+ SKIP_TO_QUEUE_ITEM, queueItemId, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToQueueItemCalled);
+ assertEquals(queueItemId, mCallback.mQueueItemId);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ PREPARE, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareCalled);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putString("mediaId", mediaId);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putString("query", query);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putParcelable("uri", uri);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SET_CAPTIONING_ENABLED, ENABLED, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetCaptioningEnabledCalled);
+ assertEquals(ENABLED, mCallback.mCaptioningEnabled);
+
+ mCallback.reset();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ callTransportControlsMethod(
+ SET_REPEAT_MODE, repeatMode, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(repeatMode, mCallback.mRepeatMode);
+
+ mCallback.reset();
+ final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+ callTransportControlsMethod(
+ SET_SHUFFLE_MODE, shuffleMode, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetShuffleModeCalled);
+ assertEquals(shuffleMode, mCallback.mShuffleMode);
+ }
+ }
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private long mSeekPosition;
+ private long mQueueItemId;
+ private RatingCompat mRating;
+ private String mMediaId;
+ private String mQuery;
+ private Uri mUri;
+ private String mAction;
+ private String mCommand;
+ private Bundle mExtras;
+ private ResultReceiver mCommandCallback;
+ private boolean mCaptioningEnabled;
+ private int mRepeatMode;
+ private int mShuffleMode;
+ private int mQueueIndex;
+ private MediaDescriptionCompat mQueueDescription;
+ private List<MediaSessionCompat.QueueItem> mQueue = new ArrayList<>();
+
+ private boolean mOnPlayCalled;
+ private boolean mOnPauseCalled;
+ private boolean mOnStopCalled;
+ private boolean mOnFastForwardCalled;
+ private boolean mOnRewindCalled;
+ private boolean mOnSkipToPreviousCalled;
+ private boolean mOnSkipToNextCalled;
+ private boolean mOnSeekToCalled;
+ private boolean mOnSkipToQueueItemCalled;
+ private boolean mOnSetRatingCalled;
+ private boolean mOnPlayFromMediaIdCalled;
+ private boolean mOnPlayFromSearchCalled;
+ private boolean mOnPlayFromUriCalled;
+ private boolean mOnCustomActionCalled;
+ private boolean mOnCommandCalled;
+ private boolean mOnPrepareCalled;
+ private boolean mOnPrepareFromMediaIdCalled;
+ private boolean mOnPrepareFromSearchCalled;
+ private boolean mOnPrepareFromUriCalled;
+ private boolean mOnSetCaptioningEnabledCalled;
+ private boolean mOnSetRepeatModeCalled;
+ private boolean mOnSetShuffleModeCalled;
+ private boolean mOnAddQueueItemCalled;
+ private boolean mOnAddQueueItemAtCalled;
+ private boolean mOnRemoveQueueItemCalled;
+
+ public void reset() {
+ mSeekPosition = -1;
+ mQueueItemId = -1;
+ mRating = null;
+ mMediaId = null;
+ mQuery = null;
+ mUri = null;
+ mAction = null;
+ mExtras = null;
+ mCommand = null;
+ mCommandCallback = null;
+ mCaptioningEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+ mQueueIndex = -1;
+ mQueueDescription = null;
+
+ mOnPlayCalled = false;
+ mOnPauseCalled = false;
+ mOnStopCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSkipToPreviousCalled = false;
+ mOnSkipToNextCalled = false;
+ mOnSkipToQueueItemCalled = false;
+ mOnSeekToCalled = false;
+ mOnSetRatingCalled = false;
+ mOnPlayFromMediaIdCalled = false;
+ mOnPlayFromSearchCalled = false;
+ mOnPlayFromUriCalled = false;
+ mOnCustomActionCalled = false;
+ mOnCommandCalled = false;
+ mOnPrepareCalled = false;
+ mOnPrepareFromMediaIdCalled = false;
+ mOnPrepareFromSearchCalled = false;
+ mOnPrepareFromUriCalled = false;
+ mOnSetCaptioningEnabledCalled = false;
+ mOnSetRepeatModeCalled = false;
+ mOnSetShuffleModeCalled = false;
+ mOnAddQueueItemCalled = false;
+ mOnAddQueueItemAtCalled = false;
+ mOnRemoveQueueItemCalled = false;
+ }
+
+ @Override
+ public void onPlay() {
+ synchronized (mWaitLock) {
+ mOnPlayCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ synchronized (mWaitLock) {
+ mOnPauseCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (mWaitLock) {
+ mOnStopCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ synchronized (mWaitLock) {
+ mOnFastForwardCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ synchronized (mWaitLock) {
+ mOnRewindCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ synchronized (mWaitLock) {
+ mOnSkipToPreviousCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ synchronized (mWaitLock) {
+ mOnSkipToNextCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ synchronized (mWaitLock) {
+ mOnSeekToCalled = true;
+ mSeekPosition = pos;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRating(RatingCompat rating) {
+ synchronized (mWaitLock) {
+ mOnSetRatingCalled = true;
+ mRating = rating;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnCustomActionCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(long id) {
+ synchronized (mWaitLock) {
+ mOnSkipToQueueItemCalled = true;
+ mQueueItemId = id;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+ synchronized (mWaitLock) {
+ mOnCommandCalled = true;
+ mCommand = command;
+ mExtras = extras;
+ mCommandCallback = cb;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepare() {
+ synchronized (mWaitLock) {
+ mOnPrepareCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description) {
+ synchronized (mWaitLock) {
+ mOnAddQueueItemCalled = true;
+ mQueueDescription = description;
+ mQueue.add(new MediaSessionCompat.QueueItem(description, mQueue.size()));
+ mSession.setQueue(mQueue);
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+ synchronized (mWaitLock) {
+ mOnAddQueueItemAtCalled = true;
+ mQueueIndex = index;
+ mQueueDescription = description;
+ mQueue.add(index, new MediaSessionCompat.QueueItem(description, mQueue.size()));
+ mSession.setQueue(mQueue);
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRemoveQueueItem(MediaDescriptionCompat description) {
+ synchronized (mWaitLock) {
+ mOnRemoveQueueItemCalled = true;
+ String mediaId = description.getMediaId();
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ if (mediaId.equals(mQueue.get(i).getDescription().getMediaId())) {
+ mQueueDescription = mQueue.remove(i).getDescription();
+ mSession.setQueue(mQueue);
+ break;
+ }
+ }
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetCaptioningEnabled(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnSetCaptioningEnabledCalled = true;
+ mCaptioningEnabled = enabled;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ synchronized (mWaitLock) {
+ mOnSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
new file mode 100644
index 0000000..57364b7
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_ACTIVE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.ACTION_CALL_MEDIA_SESSION_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import java.util.List;
+
+public class ServiceBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
+ StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ switch (method) {
+ case NOTIFY_CHILDREN_CHANGED:
+ service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
+ break;
+ case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
+ service.sendDelayedNotifyChildrenChanged();
+ break;
+ case SEND_DELAYED_ITEM_LOADED:
+ service.sendDelayedItemLoaded();
+ break;
+ case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
+ service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case CUSTOM_ACTION_SEND_ERROR:
+ service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case CUSTOM_ACTION_SEND_RESULT:
+ service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case SET_SESSION_TOKEN:
+ StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
+ .callSetSessionToken();
+ break;
+ }
+ } else if (ACTION_CALL_MEDIA_SESSION_METHOD.equals(intent.getAction()) && extras != null) {
+ MediaSessionCompat session = StubMediaBrowserServiceCompat.sSession;
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ switch (method) {
+ case SET_EXTRAS:
+ session.setExtras(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case SET_FLAGS:
+ session.setFlags(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_METADATA:
+ session.setMetadata((MediaMetadataCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_PLAYBACK_STATE:
+ session.setPlaybackState(
+ (PlaybackStateCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_QUEUE:
+ List<QueueItem> items = extras.getParcelableArrayList(KEY_ARGUMENT);
+ session.setQueue(items);
+ break;
+ case SET_QUEUE_TITLE:
+ session.setQueueTitle(extras.getCharSequence(KEY_ARGUMENT));
+ break;
+ case SET_SESSION_ACTIVITY:
+ session.setSessionActivity((PendingIntent) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_CAPTIONING_ENABLED:
+ session.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case SET_REPEAT_MODE:
+ session.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_SHUFFLE_MODE:
+ session.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SEND_SESSION_EVENT:
+ Bundle arguments = extras.getBundle(KEY_ARGUMENT);
+ session.sendSessionEvent(
+ arguments.getString("event"), arguments.getBundle("extras"));
+ break;
+ case SET_ACTIVE:
+ session.setActive(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case RELEASE:
+ session.release();
+ break;
+ case SET_PLAYBACK_TO_LOCAL:
+ session.setPlaybackToLocal(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_PLAYBACK_TO_REMOTE:
+ ParcelableVolumeInfo volumeInfo = extras.getParcelable(KEY_ARGUMENT);
+ session.setPlaybackToRemote(new VolumeProviderCompat(
+ volumeInfo.controlType,
+ volumeInfo.maxVolume,
+ volumeInfo.currentVolume) {});
+ break;
+ case SET_RATING_TYPE:
+ session.setRatingType(RatingCompat.RATING_5_STARS);
+ break;
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
new file mode 100644
index 0000000..7032a0b
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INCLUDE_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
+
+ public static StubMediaBrowserServiceCompat sInstance;
+
+ public static MediaSessionCompat sSession;
+ private Bundle mExtras;
+ private Result<List<MediaItem>> mPendingLoadChildrenResult;
+ private Result<MediaItem> mPendingLoadItemResult;
+ private Bundle mPendingRootHints;
+
+ public Bundle mCustomActionExtras;
+ public Result<Bundle> mCustomActionResult;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ sInstance = this;
+ sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
+ setSessionToken(sSession.getSessionToken());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ sSession.release();
+ sSession = null;
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ mExtras = new Bundle();
+ mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+ return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
+ }
+
+ @Override
+ public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result) {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ if (MEDIA_ID_ROOT.equals(parentId)) {
+ Bundle rootHints = getBrowserRootHints();
+ for (String id : MEDIA_ID_CHILDREN) {
+ mediaItems.add(createMediaItem(id));
+ }
+ result.sendResult(mediaItems);
+ } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentId)) {
+ Assert.assertNull(mPendingLoadChildrenResult);
+ mPendingLoadChildrenResult = result;
+ mPendingRootHints = getBrowserRootHints();
+ result.detach();
+ } else if (MEDIA_ID_INVALID.equals(parentId)) {
+ result.sendResult(null);
+ }
+ }
+
+ @Override
+ public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaItem>> result,
+ @NonNull Bundle options) {
+ if (MEDIA_ID_INCLUDE_METADATA.equals(parentId)) {
+ // Test unparcelling the Bundle.
+ MediaMetadataCompat metadata = options.getParcelable(MEDIA_METADATA);
+ if (metadata == null) {
+ super.onLoadChildren(parentId, result, options);
+ } else {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ mediaItems.add(new MediaItem(metadata.getDescription(), MediaItem.FLAG_PLAYABLE));
+ result.sendResult(mediaItems);
+ }
+ } else {
+ super.onLoadChildren(parentId, result, options);
+ }
+ }
+
+ @Override
+ public void onLoadItem(String itemId, Result<MediaItem> result) {
+ if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
+ mPendingLoadItemResult = result;
+ mPendingRootHints = getBrowserRootHints();
+ result.detach();
+ return;
+ }
+
+ if (MEDIA_ID_INVALID.equals(itemId)) {
+ result.sendResult(null);
+ return;
+ }
+
+ for (String id : MEDIA_ID_CHILDREN) {
+ if (id.equals(itemId)) {
+ result.sendResult(createMediaItem(id));
+ return;
+ }
+ }
+
+ // Test the case where onLoadItem is not implemented.
+ super.onLoadItem(itemId, result);
+ }
+
+ @Override
+ public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+ if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
+ result.sendResult(Collections.<MediaItem>emptyList());
+ } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
+ result.sendResult(null);
+ } else if (SEARCH_QUERY.equals(query)) {
+ List<MediaItem> items = new ArrayList<>();
+ for (String id : MEDIA_ID_CHILDREN) {
+ if (id.contains(query)) {
+ items.add(createMediaItem(id));
+ }
+ }
+ result.sendResult(items);
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
+ mCustomActionResult = result;
+ mCustomActionExtras = extras;
+ if (CUSTOM_ACTION_FOR_ERROR.equals(action)) {
+ result.sendError(null);
+ } else if (CUSTOM_ACTION.equals(action)) {
+ result.detach();
+ }
+ }
+
+ public void sendDelayedNotifyChildrenChanged() {
+ if (mPendingLoadChildrenResult != null) {
+ mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
+ mPendingRootHints = null;
+ mPendingLoadChildrenResult = null;
+ }
+ }
+
+ public void sendDelayedItemLoaded() {
+ if (mPendingLoadItemResult != null) {
+ mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
+ MediaItem.FLAG_BROWSABLE));
+ mPendingRootHints = null;
+ mPendingLoadItemResult = null;
+ }
+ }
+
+ private MediaItem createMediaItem(String id) {
+ return new MediaItem(new MediaDescriptionCompat.Builder().setMediaId(id).build(),
+ MediaItem.FLAG_BROWSABLE);
+ }
+}
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
new file mode 100644
index 0000000..509e13f
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import java.util.List;
+
+/**
+ * Stub implementation of {@link MediaBrowserServiceCompat}.
+ * This implementation does not call
+ * {@link MediaBrowserServiceCompat#setSessionToken(MediaSessionCompat.Token)} in its
+ * {@link android.app.Service#onCreate}.
+ */
+public class StubMediaBrowserServiceCompatWithDelayedMediaSession extends
+ MediaBrowserServiceCompat {
+
+ static StubMediaBrowserServiceCompatWithDelayedMediaSession sInstance;
+ private MediaSessionCompat mSession;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ sInstance = this;
+ mSession = new MediaSessionCompat(
+ this, "StubMediaBrowserServiceCompatWithDelayedMediaSession");
+ }
+
+ @Nullable
+ @Override
+ public BrowserRoot onGetRoot(@NonNull String clientPackageName,
+ int clientUid, @Nullable Bundle rootHints) {
+ return new BrowserRoot("StubRootId", null);
+ }
+
+ @Override
+ public void onLoadChildren(@NonNull String parentId,
+ @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+ result.detach();
+ }
+
+ public void callSetSessionToken() {
+ setSessionToken(mSession.getSessionToken());
+ }
+}
diff --git a/media-compat/version-compat-tests/lib/AndroidManifest.xml b/media-compat/version-compat-tests/lib/AndroidManifest.xml
new file mode 100644
index 0000000..857e61c
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="android.support.mediacompat.testlib"/>
diff --git a/media-compat/version-compat-tests/lib/build.gradle b/media-compat/version-compat-tests/lib/build.gradle
new file mode 100644
index 0000000..a9be453
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/build.gradle
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+}
+
+supportLibrary {
+ legacySourceLocation = true
+}
diff --git a/media-compat/version-compat-tests/lib/lint-baseline.xml b/media-compat/version-compat-tests/lib/lint-baseline.xml
new file mode 100644
index 0000000..4dd17af
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/lint-baseline.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<issues format="4" by="lint 3.0.0">
+
+</issues>
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
new file mode 100644
index 0000000..f961308
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib;
+
+/**
+ * Constants for testing the media browser and service.
+ */
+public class MediaBrowserConstants {
+
+ // MediaBrowserServiceCompat methods.
+ public static final int NOTIFY_CHILDREN_CHANGED = 1;
+ public static final int SEND_DELAYED_NOTIFY_CHILDREN_CHANGED = 2;
+ public static final int SEND_DELAYED_ITEM_LOADED = 3;
+ public static final int CUSTOM_ACTION_SEND_PROGRESS_UPDATE = 4;
+ public static final int CUSTOM_ACTION_SEND_ERROR = 5;
+ public static final int CUSTOM_ACTION_SEND_RESULT = 6;
+ public static final int SET_SESSION_TOKEN = 7;
+
+ public static final String MEDIA_ID_ROOT = "test_media_id_root";
+ public static final String MEDIA_ID_INVALID = "test_media_id_invalid";
+ public static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed";
+ public static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED =
+ "test_media_id_on_load_item_not_implemented";
+ public static final String MEDIA_ID_INCLUDE_METADATA = "test_media_id_include_metadata";
+
+ public static final String EXTRAS_KEY = "test_extras_key";
+ public static final String EXTRAS_VALUE = "test_extras_value";
+
+ public static final String MEDIA_METADATA = "test_media_metadata";
+
+ public static final String SEARCH_QUERY = "children_2";
+ public static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
+ public static final String SEARCH_QUERY_FOR_ERROR = "query for error";
+
+ public static final String CUSTOM_ACTION = "CUSTOM_ACTION";
+ public static final String CUSTOM_ACTION_FOR_ERROR = "CUSTOM_ACTION_FOR_ERROR";
+
+ public static final String TEST_KEY_1 = "key_1";
+ public static final String TEST_VALUE_1 = "value_1";
+ public static final String TEST_KEY_2 = "key_2";
+ public static final String TEST_VALUE_2 = "value_2";
+ public static final String TEST_KEY_3 = "key_3";
+ public static final String TEST_VALUE_3 = "value_3";
+ public static final String TEST_KEY_4 = "key_4";
+ public static final String TEST_VALUE_4 = "value_4";
+
+ public static final String[] MEDIA_ID_CHILDREN = new String[]{
+ "test_media_id_children_0", "test_media_id_children_1",
+ "test_media_id_children_2", "test_media_id_children_3",
+ MEDIA_ID_CHILDREN_DELAYED
+ };
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java
new file mode 100644
index 0000000..5fa086b
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib;
+
+/**
+ * Constants for testing the media controller.
+ */
+public class MediaControllerConstants {
+
+ // MediaControllerCompat methods.
+ public static final int SEND_COMMAND = 201;
+ public static final int ADD_QUEUE_ITEM = 202;
+ public static final int ADD_QUEUE_ITEM_WITH_INDEX = 203;
+ public static final int REMOVE_QUEUE_ITEM = 204;
+
+ // TransportControls methods.
+ public static final int PLAY = 301;
+ public static final int PAUSE = 302;
+ public static final int STOP = 303;
+ public static final int FAST_FORWARD = 304;
+ public static final int REWIND = 305;
+ public static final int SKIP_TO_PREVIOUS = 306;
+ public static final int SKIP_TO_NEXT = 307;
+ public static final int SEEK_TO = 308;
+ public static final int SET_RATING = 309;
+ public static final int PLAY_FROM_MEDIA_ID = 310;
+ public static final int PLAY_FROM_SEARCH = 311;
+ public static final int PLAY_FROM_URI = 312;
+ public static final int SEND_CUSTOM_ACTION = 313;
+ public static final int SEND_CUSTOM_ACTION_PARCELABLE = 314;
+ public static final int SKIP_TO_QUEUE_ITEM = 315;
+ public static final int PREPARE = 316;
+ public static final int PREPARE_FROM_MEDIA_ID = 317;
+ public static final int PREPARE_FROM_SEARCH = 318;
+ public static final int PREPARE_FROM_URI = 319;
+ public static final int SET_CAPTIONING_ENABLED = 320;
+ public static final int SET_REPEAT_MODE = 321;
+ public static final int SET_SHUFFLE_MODE = 322;
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java
new file mode 100644
index 0000000..cbdccc1
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib;
+
+/**
+ * Constants for testing the media session.
+ */
+public class MediaSessionConstants {
+
+ // MediaSessionCompat methods.
+ public static final int SET_EXTRAS = 101;
+ public static final int SET_FLAGS = 102;
+ public static final int SET_METADATA = 103;
+ public static final int SET_PLAYBACK_STATE = 104;
+ public static final int SET_QUEUE = 105;
+ public static final int SET_QUEUE_TITLE = 106;
+ public static final int SET_SESSION_ACTIVITY = 107;
+ public static final int SET_CAPTIONING_ENABLED = 108;
+ public static final int SET_REPEAT_MODE = 109;
+ public static final int SET_SHUFFLE_MODE = 110;
+ public static final int SEND_SESSION_EVENT = 112;
+ public static final int SET_ACTIVE = 113;
+ public static final int RELEASE = 114;
+ public static final int SET_PLAYBACK_TO_LOCAL = 115;
+ public static final int SET_PLAYBACK_TO_REMOTE = 116;
+ public static final int SET_RATING_TYPE = 117;
+
+ public static final String TEST_SESSION_TAG = "test-session-tag";
+ public static final String TEST_KEY = "test-key";
+ public static final String TEST_VALUE = "test-val";
+ public static final String TEST_SESSION_EVENT = "test-session-event";
+ public static final String TEST_COMMAND = "test-command";
+ public static final int TEST_FLAGS = 5;
+ public static final int TEST_CURRENT_VOLUME = 10;
+ public static final int TEST_MAX_VOLUME = 11;
+ public static final long TEST_QUEUE_ID_1 = 10L;
+ public static final long TEST_QUEUE_ID_2 = 20L;
+ public static final String TEST_MEDIA_ID_1 = "media_id_1";
+ public static final String TEST_MEDIA_ID_2 = "media_id_2";
+ public static final long TEST_ACTION = 55L;
+
+ public static final int TEST_ERROR_CODE = 0x3;
+ public static final String TEST_ERROR_MSG = "test-error-msg";
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
new file mode 100644
index 0000000..4b217b1
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib;
+
+/**
+ * Constants for getting support library version information.
+ */
+public class VersionConstants {
+ public static final String KEY_CLIENT_VERSION = "client_version";
+ public static final String KEY_SERVICE_VERSION = "service_version";
+
+ public static final String VERSION_TOT = "tot";
+ public static final String VERSION_PREVIOUS = "previous";
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java
new file mode 100644
index 0000000..bbf9752
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+
+/**
+ * Methods and constants used for sending intent between client and service apps.
+ */
+public class IntentUtil {
+
+ public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service.ServiceBroadcastReceiver");
+ public static final ComponentName CLIENT_RECEIVER_COMPONENT_NAME = new ComponentName(
+ "android.support.mediacompat.client.test",
+ "android.support.mediacompat.client.ClientBroadcastReceiver");
+
+ public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD =
+ "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD";
+ public static final String ACTION_CALL_MEDIA_SESSION_METHOD =
+ "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD";
+ public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD =
+ "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD";
+ public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD =
+ "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD";
+
+ public static final String KEY_METHOD_ID = "method_id";
+ public static final String KEY_ARGUMENT = "argument";
+ public static final String KEY_SESSION_TOKEN = "session_token";
+
+ /**
+ * Calls a method of MediaBrowserService. Used by client app.
+ */
+ public static void callMediaBrowserServiceMethod(int methodId, Object arg, Context context) {
+ Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
+ intent.setAction(ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD);
+ if (Build.VERSION.SDK_INT >= 16) {
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ }
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * Calls a method of MediaSession. Used by client app.
+ */
+ public static void callMediaSessionMethod(int methodId, Object arg, Context context) {
+ Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
+ intent.setAction(ACTION_CALL_MEDIA_SESSION_METHOD);
+ if (Build.VERSION.SDK_INT >= 16) {
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ }
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * Calls a method of MediaController. Used by service app.
+ */
+ public static void callMediaControllerMethod(
+ int methodId, Object arg, Context context, Parcelable token) {
+ Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg);
+ intent.setAction(ACTION_CALL_MEDIA_CONTROLLER_METHOD);
+ intent.putExtra(KEY_SESSION_TOKEN, token);
+ if (Build.VERSION.SDK_INT >= 16) {
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ }
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * Calls a method of TransportControls. Used by service app.
+ */
+ public static void callTransportControlsMethod(
+ int methodId, Object arg, Context context, Parcelable token) {
+ Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg);
+ intent.setAction(ACTION_CALL_TRANSPORT_CONTROLS_METHOD);
+ intent.putExtra(KEY_SESSION_TOKEN, token);
+ if (Build.VERSION.SDK_INT >= 16) {
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ }
+ context.sendBroadcast(intent);
+ }
+
+ private static Intent createIntent(ComponentName componentName, int methodId, Object arg) {
+ Intent intent = new Intent();
+ intent.setComponent(componentName);
+ intent.putExtra(KEY_METHOD_ID, methodId);
+
+ if (arg instanceof String) {
+ intent.putExtra(KEY_ARGUMENT, (String) arg);
+ } else if (arg instanceof Integer) {
+ intent.putExtra(KEY_ARGUMENT, (int) arg);
+ } else if (arg instanceof Long) {
+ intent.putExtra(KEY_ARGUMENT, (long) arg);
+ } else if (arg instanceof Boolean) {
+ intent.putExtra(KEY_ARGUMENT, (boolean) arg);
+ } else if (arg instanceof Parcelable) {
+ intent.putExtra(KEY_ARGUMENT, (Parcelable) arg);
+ } else if (arg instanceof ArrayList<?>) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(KEY_ARGUMENT, (ArrayList<? extends Parcelable>) arg);
+ intent.putExtras(bundle);
+ } else if (arg instanceof Bundle) {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(KEY_ARGUMENT, (Bundle) arg);
+ intent.putExtras(bundle);
+ }
+ return intent;
+ }
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java
new file mode 100644
index 0000000..3412da0
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib.util;
+
+import junit.framework.Assert;
+
+/**
+ * Utility used for testing that allows to poll for a certain condition to happen within a timeout.
+ * (Copied from testutils/src/main/java/android/support/testutils/PollingCheck.java.)
+ */
+public abstract class PollingCheck {
+ private static final long DEFAULT_TIMEOUT = 3000;
+ private static final long TIME_SLICE = 50;
+ private final long mTimeout;
+
+ /**
+ * The condition that the PollingCheck should use to proceed successfully.
+ */
+ public interface PollingCheckCondition {
+ /**
+ * @return Whether the polling condition has been met.
+ */
+ boolean canProceed();
+ }
+
+ public PollingCheck(long timeout) {
+ mTimeout = timeout;
+ }
+
+ protected abstract boolean check();
+
+ /**
+ * Start running the polling check.
+ */
+ public void run() {
+ if (check()) {
+ return;
+ }
+
+ long timeout = mTimeout;
+ while (timeout > 0) {
+ try {
+ Thread.sleep(TIME_SLICE);
+ } catch (InterruptedException e) {
+ Assert.fail("unexpected InterruptedException");
+ }
+
+ if (check()) {
+ return;
+ }
+
+ timeout -= TIME_SLICE;
+ }
+
+ Assert.fail("unexpected timeout");
+ }
+
+ /**
+ * Instantiate and start polling for a given condition with a default 3000ms timeout.
+ * @param condition The condition to check for success.
+ */
+ public static void waitFor(final PollingCheckCondition condition) {
+ new PollingCheck(DEFAULT_TIMEOUT) {
+ @Override
+ protected boolean check() {
+ return condition.canProceed();
+ }
+ }.run();
+ }
+
+ /**
+ * Instantiate and start polling for a given condition.
+ * @param timeout Time out in ms
+ * @param condition The condition to check for success.
+ */
+ public static void waitFor(long timeout, final PollingCheckCondition condition) {
+ new PollingCheck(timeout) {
+ @Override
+ protected boolean check() {
+ return condition.canProceed();
+ }
+ }.run();
+ }
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java
new file mode 100644
index 0000000..d105510
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+
+import android.os.Bundle;
+
+/**
+ * Utility methods used for testing.
+ */
+public final class TestUtil {
+
+ /**
+ * Asserts that two Bundles are equal.
+ */
+ public static void assertBundleEquals(Bundle expected, Bundle observed) {
+ if (expected == null || observed == null) {
+ assertSame(expected, observed);
+ }
+ assertEquals(expected.size(), observed.size());
+ for (String key : expected.keySet()) {
+ assertEquals(expected.get(key), observed.get(key));
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/client/AndroidManifest.xml b/media-compat/version-compat-tests/previous/client/AndroidManifest.xml
new file mode 100644
index 0000000..9724d2b
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="android.support.mediacompat.client"/>
diff --git a/media-compat/version-compat-tests/previous/client/build.gradle b/media-compat/version-compat-tests/previous/client/build.gradle
new file mode 100644
index 0000000..01b3847
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ androidTestImplementation project(':support-media-compat-test-lib')
+ androidTestImplementation "com.android.support:support-media-compat:27.0.1"
+
+ androidTestImplementation(libs.test_runner)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+}
+
+supportLibrary {
+ legacySourceLocation = true
+}
\ No newline at end of file
diff --git a/media-compat/version-compat-tests/previous/client/lint-baseline.xml b/media-compat/version-compat-tests/previous/client/lint-baseline.xml
new file mode 100644
index 0000000..ed7ade1
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/lint-baseline.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml b/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml
new file mode 100644
index 0000000..afe1865
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.mediacompat.client.test">
+ <application android:supportsRtl="true">
+ <receiver android:name="android.support.mediacompat.client.ClientBroadcastReceiver">
+ <intent-filter>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_CONTROLLER_METHOD"/>
+ <action android:name="android.support.mediacompat.service.action.CALL_TRANSPORT_CONTROLS_METHOD"/>
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/media-compat/version-compat-tests/previous/client/tests/NO_DOCS b/media-compat/version-compat-tests/previous/client/tests/NO_DOCS
new file mode 100644
index 0000000..61c9b1a
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
new file mode 100644
index 0000000..3166e55
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_MEDIA_CONTROLLER_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_TRANSPORT_CONTROLS_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_SESSION_TOKEN;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+public class ClientBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle extras = intent.getExtras();
+ MediaControllerCompat controller;
+ try {
+ controller = new MediaControllerCompat(context,
+ (MediaSessionCompat.Token) extras.getParcelable(KEY_SESSION_TOKEN));
+ } catch (RemoteException ex) {
+ // Do nothing.
+ return;
+ }
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ if (ACTION_CALL_MEDIA_CONTROLLER_METHOD.equals(intent.getAction()) && extras != null) {
+ Bundle arguments;
+ switch (method) {
+ case SEND_COMMAND:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controller.sendCommand(
+ arguments.getString("command"),
+ arguments.getBundle("extras"),
+ new ResultReceiver(null));
+ break;
+ case ADD_QUEUE_ITEM:
+ controller.addQueueItem(
+ (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case ADD_QUEUE_ITEM_WITH_INDEX:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controller.addQueueItem(
+ (MediaDescriptionCompat) arguments.getParcelable("description"),
+ arguments.getInt("index"));
+ break;
+ case REMOVE_QUEUE_ITEM:
+ controller.removeQueueItem(
+ (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ }
+ } else if (ACTION_CALL_TRANSPORT_CONTROLS_METHOD.equals(intent.getAction())
+ && extras != null) {
+ TransportControls controls = controller.getTransportControls();
+ Bundle arguments;
+ switch (method) {
+ case PLAY:
+ controls.play();
+ break;
+ case PAUSE:
+ controls.pause();
+ break;
+ case STOP:
+ controls.stop();
+ break;
+ case FAST_FORWARD:
+ controls.fastForward();
+ break;
+ case REWIND:
+ controls.rewind();
+ break;
+ case SKIP_TO_PREVIOUS:
+ controls.skipToPrevious();
+ break;
+ case SKIP_TO_NEXT:
+ controls.skipToNext();
+ break;
+ case SEEK_TO:
+ controls.seekTo(extras.getLong(KEY_ARGUMENT));
+ break;
+ case SET_RATING:
+ controls.setRating((RatingCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case PLAY_FROM_MEDIA_ID:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromMediaId(
+ arguments.getString("mediaId"),
+ arguments.getBundle("extras"));
+ break;
+ case PLAY_FROM_SEARCH:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromSearch(
+ arguments.getString("query"),
+ arguments.getBundle("extras"));
+ break;
+ case PLAY_FROM_URI:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.playFromUri(
+ (Uri) arguments.getParcelable("uri"),
+ arguments.getBundle("extras"));
+ break;
+ case SEND_CUSTOM_ACTION:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.sendCustomAction(
+ arguments.getString("action"),
+ arguments.getBundle("extras"));
+ break;
+ case SEND_CUSTOM_ACTION_PARCELABLE:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.sendCustomAction(
+ (PlaybackStateCompat.CustomAction)
+ arguments.getParcelable("action"),
+ arguments.getBundle("extras"));
+ break;
+ case SKIP_TO_QUEUE_ITEM:
+ controls.skipToQueueItem(extras.getLong(KEY_ARGUMENT));
+ break;
+ case PREPARE:
+ controls.prepare();
+ break;
+ case PREPARE_FROM_MEDIA_ID:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromMediaId(
+ arguments.getString("mediaId"),
+ arguments.getBundle("extras"));
+ break;
+ case PREPARE_FROM_SEARCH:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromSearch(
+ arguments.getString("query"),
+ arguments.getBundle("extras"));
+ break;
+ case PREPARE_FROM_URI:
+ arguments = extras.getBundle(KEY_ARGUMENT);
+ controls.prepareFromUri(
+ (Uri) arguments.getParcelable("uri"),
+ arguments.getBundle("extras"));
+ break;
+ case SET_CAPTIONING_ENABLED:
+ controls.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case SET_REPEAT_MODE:
+ controls.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_SHUFFLE_MODE:
+ controls.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
new file mode 100644
index 0000000..325ff72
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -0,0 +1,1047 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_4;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_4;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaBrowserServiceMethod;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.content.ComponentName;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserCompatTest {
+
+ private static final String TAG = "MediaBrowserCompatTest";
+
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 300L;
+
+ /**
+ * To check {@link MediaBrowserCompat#unsubscribe} works properly,
+ * we notify to the browser after the unsubscription that the media items have changed.
+ * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
+ *
+ * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
+ * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
+ * 50ms.
+ * So we make the thread sleep for 100ms to properly check that the callback is not called.
+ */
+ private static final long SLEEP_MS = 100L;
+ private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+ private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
+ new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service"
+ + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+ private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
+ "invalid.package", "invalid.ServiceClassName");
+
+ private String mServiceVersion;
+ private MediaBrowserCompat mMediaBrowser;
+ private StubConnectionCallback mConnectionCallback;
+ private StubSubscriptionCallback mSubscriptionCallback;
+ private StubItemCallback mItemCallback;
+ private StubSearchCallback mSearchCallback;
+ private CustomActionCallback mCustomActionCallback;
+ private Bundle mRootHints;
+
+ @Before
+ public void setUp() {
+ // The version of the service app is provided through the instrumentation arguments.
+ mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+ Log.d(TAG, "Service app version: " + mServiceVersion);
+
+ mConnectionCallback = new StubConnectionCallback();
+ mSubscriptionCallback = new StubSubscriptionCallback();
+ mItemCallback = new StubItemCallback();
+ mSearchCallback = new StubSearchCallback();
+ mCustomActionCallback = new CustomActionCallback();
+
+ mRootHints = new Bundle();
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
+ mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() {
+ if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+ mMediaBrowser.disconnect();
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testBrowserRoot() {
+ final String id = "test-id";
+ final String key = "test-key";
+ final String val = "test-val";
+ final Bundle extras = new Bundle();
+ extras.putString(key, val);
+
+ MediaBrowserServiceCompat.BrowserRoot browserRoot =
+ new MediaBrowserServiceCompat.BrowserRoot(id, extras);
+ assertEquals(id, browserRoot.getRootId());
+ assertEquals(val, browserRoot.getExtras().getString(key));
+ }
+
+ @Test
+ @SmallTest
+ public void testMediaBrowser() throws Exception {
+ assertFalse(mMediaBrowser.isConnected());
+
+ connectMediaBrowserService();
+ assertTrue(mMediaBrowser.isConnected());
+
+ assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+ assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+ assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
+
+ mMediaBrowser.disconnect();
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return !mMediaBrowser.isConnected();
+ }
+ }.run();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetServiceComponentBeforeConnection() {
+ try {
+ ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
+ fail();
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectionFailed() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_INVALID_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ }
+ assertEquals(1, mConnectionCallback.mConnectionFailedCount);
+ assertEquals(0, mConnectionCallback.mConnectedCount);
+ assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectTwice() throws Exception {
+ connectMediaBrowserService();
+ try {
+ mMediaBrowser.connect();
+ fail();
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testReconnection() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser.connect();
+ // Reconnect before the first connection was established.
+ mMediaBrowser.disconnect();
+ mMediaBrowser.connect();
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(1, mConnectionCallback.mConnectedCount);
+ }
+
+ // Test subscribe.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+
+ synchronized (mItemCallback.mWaitLock) {
+ // Test getItem.
+ mItemCallback.reset();
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+
+ // Reconnect after connection was established.
+ mMediaBrowser.disconnect();
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ // Test getItem.
+ mItemCallback.reset();
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testConnectionCallbackNotCalledAfterDisconnect() {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser.connect();
+ mMediaBrowser.disconnect();
+ mConnectionCallback.reset();
+ }
+ });
+
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+ assertEquals(0, mConnectionCallback.mConnectedCount);
+ assertEquals(0, mConnectionCallback.mConnectionFailedCount);
+ assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribe() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+ assertEquals(MEDIA_ID_CHILDREN.length, mSubscriptionCallback.mLastChildMediaItems.size());
+ for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
+ assertEquals(MEDIA_ID_CHILDREN[i],
+ mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+ }
+
+ // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+ mSubscriptionCallback.reset(1);
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+
+ // Test unsubscribe.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ // onChildrenLoaded should not be called.
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribeWithOptions() throws Exception {
+ connectMediaBrowserService();
+ final int pageSize = 3;
+ final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+
+ for (int page = 0; page <= lastPage; ++page) {
+ mSubscriptionCallback.reset(1);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
+ assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+ if (page != lastPage) {
+ assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
+ } else {
+ assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
+ mSubscriptionCallback.mLastChildMediaItems.size());
+ }
+ // Check whether all the items in the current page are loaded.
+ for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
+ assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
+ mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+ }
+
+ // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+ mSubscriptionCallback.reset(page + 1);
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(page + 1, mSubscriptionCallback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Test unsubscribe with callback argument.
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+ // onChildrenLoaded should not be called.
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @MediumTest
+ public void testSubscribeDelayedItems() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_CHILDREN_DELAYED, mSubscriptionCallback);
+ mSubscriptionCallback.await(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+
+ callMediaBrowserServiceMethod(
+ SEND_DELAYED_NOTIFY_CHILDREN_CHANGED, MEDIA_ID_CHILDREN_DELAYED, getContext());
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(1, mSubscriptionCallback.mChildrenLoadedCount);
+ }
+
+ @Test
+ @SmallTest
+ public void testSubscribeInvalidItem() throws Exception {
+ connectMediaBrowserService();
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+ }
+
+ @Test
+ @SmallTest
+ public void testSubscribeInvalidItemWithOptions() throws Exception {
+ connectMediaBrowserService();
+
+ final int pageSize = 5;
+ final int page = 2;
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+
+ mSubscriptionCallback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
+ mSubscriptionCallback.await(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+ assertNotNull(mSubscriptionCallback.mLastOptions);
+ assertEquals(page,
+ mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
+ assertEquals(pageSize,
+ mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
+ }
+
+ @Test
+ @MediumTest
+ public void testUnsubscribeForMultipleSubscriptions() throws Exception {
+ connectMediaBrowserService();
+ final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+ final int pageSize = 1;
+
+ // Subscribe four pages, one item per page.
+ for (int page = 0; page < 4; page++) {
+ final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+ subscriptionCallbacks.add(callback);
+
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ callback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+ callback.await(TIME_OUT_MS);
+
+ // Each onChildrenLoaded() must be called.
+ assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Reset callbacks and unsubscribe.
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ callback.reset(1);
+ }
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+ // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+ // changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+
+ // onChildrenLoaded should not be called.
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ assertEquals(0, callback.mChildrenLoadedWithOptionCount);
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
+ connectMediaBrowserService();
+ final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+ final int pageSize = 1;
+
+ // Subscribe four pages, one item per page.
+ for (int page = 0; page < 4; page++) {
+ final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+ subscriptionCallbacks.add(callback);
+
+ Bundle options = new Bundle();
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ callback.reset(1);
+ mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+ callback.await(TIME_OUT_MS);
+
+ // Each onChildrenLoaded() must be called.
+ assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+ }
+
+ // Unsubscribe existing subscriptions one-by-one.
+ final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
+ for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
+ // Reset callbacks
+ for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+ callback.reset(1);
+ }
+
+ // Remove one subscription
+ mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
+ subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
+
+ // Make StubMediaBrowserServiceCompat notify that the children are changed.
+ callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+ try {
+ Thread.sleep(SLEEP_MS);
+ } catch (InterruptedException e) {
+ fail("Unexpected InterruptedException occurred.");
+ }
+
+ // Only the remaining subscriptionCallbacks should be called.
+ for (int j = 0; j < 4; j++) {
+ int childrenLoadedWithOptionsCount = subscriptionCallbacks
+ .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
+ if (j <= i) {
+ assertEquals(0, childrenLoadedWithOptionsCount);
+ } else {
+ assertEquals(1, childrenLoadedWithOptionsCount);
+ }
+ }
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItem() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNotNull(mItemCallback.mLastMediaItem);
+ assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testGetItemDelayed() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
+ mItemCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertNull(mItemCallback.mLastMediaItem);
+
+ mItemCallback.reset();
+ callMediaBrowserServiceMethod(SEND_DELAYED_ITEM_LOADED, new Bundle(), getContext());
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNotNull(mItemCallback.mLastMediaItem);
+ assertEquals(MEDIA_ID_CHILDREN_DELAYED, mItemCallback.mLastMediaItem.getMediaId());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
+ connectMediaBrowserService();
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetItemWhenMediaIdIsInvalid() throws Exception {
+ mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
+
+ connectMediaBrowserService();
+ synchronized (mItemCallback.mWaitLock) {
+ mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
+ mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertNull(mItemCallback.mLastMediaItem);
+ assertNull(mItemCallback.mLastErrorId);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSearch() throws Exception {
+ connectMediaBrowserService();
+
+ final String key = "test-key";
+ final String val = "test-val";
+
+ synchronized (mSearchCallback.mWaitLock) {
+ mSearchCallback.reset();
+ mMediaBrowser.search(SEARCH_QUERY_FOR_NO_RESULT, null, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertTrue(mSearchCallback.mSearchResults != null
+ && mSearchCallback.mSearchResults.size() == 0);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ mMediaBrowser.search(SEARCH_QUERY_FOR_ERROR, null, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNull(mSearchCallback.mSearchResults);
+ assertEquals(null, mSearchCallback.mSearchExtras);
+
+ mSearchCallback.reset();
+ Bundle extras = new Bundle();
+ extras.putString(key, val);
+ mMediaBrowser.search(SEARCH_QUERY, extras, mSearchCallback);
+ mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertTrue(mSearchCallback.mOnSearchResult);
+ assertNotNull(mSearchCallback.mSearchResults);
+ for (MediaItem item : mSearchCallback.mSearchResults) {
+ assertNotNull(item.getMediaId());
+ assertTrue(item.getMediaId().contains(SEARCH_QUERY));
+ }
+ assertNotNull(mSearchCallback.mSearchExtras);
+ assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCustomAction() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(
+ CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ mCustomActionCallback.reset();
+ Bundle data1 = new Bundle();
+ data1.putString(TEST_KEY_2, TEST_VALUE_2);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data1, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+ mCustomActionCallback.reset();
+ Bundle data2 = new Bundle();
+ data2.putString(TEST_KEY_3, TEST_VALUE_3);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data2, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+
+ Bundle resultData = new Bundle();
+ resultData.putString(TEST_KEY_4, TEST_VALUE_4);
+ mCustomActionCallback.reset();
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, resultData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCustomActionCallback.mOnResultCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_4, mCustomActionCallback.mData.getString(TEST_KEY_4));
+ }
+ }
+
+
+ @Test
+ @MediumTest
+ public void testSendCustomActionWithDetachedError() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(
+ CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ mCustomActionCallback.reset();
+ Bundle progressUpdateData = new Bundle();
+ progressUpdateData.putString(TEST_KEY_2, TEST_VALUE_2);
+ callMediaBrowserServiceMethod(
+ CUSTOM_ACTION_SEND_PROGRESS_UPDATE, progressUpdateData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+ mCustomActionCallback.reset();
+ Bundle errorData = new Bundle();
+ errorData.putString(TEST_KEY_3, TEST_VALUE_3);
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_ERROR, errorData, getContext());
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnErrorCalled);
+ assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+ assertNotNull(mCustomActionCallback.mExtras);
+ assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+ assertNotNull(mCustomActionCallback.mData);
+ assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testSendCustomActionWithNullCallback() throws Exception {
+ connectMediaBrowserService();
+
+ Bundle customActionExtras = new Bundle();
+ customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+ mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+ // Wait some time so that the service can get a result receiver for the custom action.
+ Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+ // These calls should not make any exceptions.
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
+ getContext());
+ callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, new Bundle(), getContext());
+ Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCustomActionWithError() throws Exception {
+ connectMediaBrowserService();
+
+ synchronized (mCustomActionCallback.mWaitLock) {
+ mMediaBrowser.sendCustomAction(CUSTOM_ACTION_FOR_ERROR, null, mCustomActionCallback);
+ mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCustomActionCallback.mOnErrorCalled);
+ }
+ }
+
+ @Test
+ @MediumTest
+ public void testDelayedSetSessionToken() throws Exception {
+ // This test has no meaning in API 21. The framework MediaBrowserService just connects to
+ // the media browser without waiting setMediaSession() to be called.
+ if (Build.VERSION.SDK_INT == 21) {
+ return;
+ }
+ final ConnectionCallbackForDelayedMediaSession callback =
+ new ConnectionCallbackForDelayedMediaSession();
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(
+ getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION,
+ callback,
+ null);
+ }
+ });
+
+ synchronized (callback.mWaitLock) {
+ mMediaBrowser.connect();
+ callback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+ assertEquals(0, callback.mConnectedCount);
+
+ callMediaBrowserServiceMethod(SET_SESSION_TOKEN, new Bundle(), getContext());
+ callback.mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(1, callback.mConnectedCount);
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ assertNotNull(mMediaBrowser.getSessionToken().getExtraBinder());
+ }
+ }
+ }
+
+ private void connectMediaBrowserService() throws Exception {
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ if (!mMediaBrowser.isConnected()) {
+ fail("Browser failed to connect!");
+ }
+ }
+ }
+
+ private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+ volatile int mConnectedCount;
+ volatile int mConnectionFailedCount;
+ volatile int mConnectionSuspendedCount;
+
+ public void reset() {
+ mConnectedCount = 0;
+ mConnectionFailedCount = 0;
+ mConnectionSuspendedCount = 0;
+ }
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mConnectedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ synchronized (mWaitLock) {
+ mConnectionFailedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ synchronized (mWaitLock) {
+ mConnectionSuspendedCount++;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+ private CountDownLatch mLatch;
+ private volatile int mChildrenLoadedCount;
+ private volatile int mChildrenLoadedWithOptionCount;
+ private volatile String mLastErrorId;
+ private volatile String mLastParentId;
+ private volatile Bundle mLastOptions;
+ private volatile List<MediaItem> mLastChildMediaItems;
+
+ public void reset(int count) {
+ mLatch = new CountDownLatch(count);
+ mChildrenLoadedCount = 0;
+ mChildrenLoadedWithOptionCount = 0;
+ mLastErrorId = null;
+ mLastParentId = null;
+ mLastOptions = null;
+ mLastChildMediaItems = null;
+ }
+
+ public boolean await(long timeoutMs) {
+ try {
+ return mLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
+ mChildrenLoadedCount++;
+ mLastParentId = parentId;
+ mLastChildMediaItems = children;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
+ @NonNull Bundle options) {
+ mChildrenLoadedWithOptionCount++;
+ mLastParentId = parentId;
+ mLastOptions = options;
+ mLastChildMediaItems = children;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull String id) {
+ mLastErrorId = id;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onError(@NonNull String id, @NonNull Bundle options) {
+ mLastErrorId = id;
+ mLastOptions = options;
+ mLatch.countDown();
+ }
+ }
+
+ private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
+ final Object mWaitLock = new Object();
+ private volatile MediaItem mLastMediaItem;
+ private volatile String mLastErrorId;
+
+ public void reset() {
+ mLastMediaItem = null;
+ mLastErrorId = null;
+ }
+
+ @Override
+ public void onItemLoaded(MediaItem item) {
+ synchronized (mWaitLock) {
+ mLastMediaItem = item;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String id) {
+ synchronized (mWaitLock) {
+ mLastErrorId = id;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class StubSearchCallback extends MediaBrowserCompat.SearchCallback {
+ final Object mWaitLock = new Object();
+ boolean mOnSearchResult;
+ Bundle mSearchExtras;
+ List<MediaItem> mSearchResults;
+
+ @Override
+ public void onSearchResult(@NonNull String query, Bundle extras,
+ @NonNull List<MediaItem> items) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = items;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSearchResult = true;
+ mSearchResults = null;
+ mSearchExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ public void reset() {
+ mOnSearchResult = false;
+ mSearchExtras = null;
+ mSearchResults = null;
+ }
+ }
+
+ private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
+ final Object mWaitLock = new Object();
+ String mAction;
+ Bundle mExtras;
+ Bundle mData;
+ boolean mOnProgressUpdateCalled;
+ boolean mOnResultCalled;
+ boolean mOnErrorCalled;
+
+ @Override
+ public void onProgressUpdate(String action, Bundle extras, Bundle data) {
+ synchronized (mWaitLock) {
+ mOnProgressUpdateCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = data;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onResult(String action, Bundle extras, Bundle resultData) {
+ synchronized (mWaitLock) {
+ mOnResultCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = resultData;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onError(String action, Bundle extras, Bundle data) {
+ synchronized (mWaitLock) {
+ mOnErrorCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mData = data;
+ mWaitLock.notify();
+ }
+ }
+
+ public void reset() {
+ mOnResultCalled = false;
+ mOnProgressUpdateCalled = false;
+ mOnErrorCalled = false;
+ mAction = null;
+ mExtras = null;
+ mData = null;
+ }
+ }
+
+ private class ConnectionCallbackForDelayedMediaSession extends
+ MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+ private int mConnectedCount = 0;
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mConnectedCount++;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
new file mode 100644
index 0000000..4466721
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.client;
+
+import static android.media.AudioManager.STREAM_MUSIC;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ACTION;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_CURRENT_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_CODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_MSG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MAX_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaSessionMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaControllerCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatCallbackTest {
+
+ private static final String TAG = "MediaControllerCompatCallbackTest";
+
+ // The maximum time to wait for an operation, that is expected to happen.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+
+ private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+ "android.support.mediacompat.service.test",
+ "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Object mWaitLock = new Object();
+
+ private String mServiceVersion;
+
+ // MediaBrowserCompat object to get the session token.
+ private MediaBrowserCompat mMediaBrowser;
+ private ConnectionCallback mConnectionCallback = new ConnectionCallback();
+
+ private MediaSessionCompat.Token mSessionToken;
+ private MediaControllerCompat mController;
+ private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
+
+ @Before
+ public void setUp() throws Exception {
+ // The version of the service app is provided through the instrumentation arguments.
+ mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+ Log.d(TAG, "Service app version: " + mServiceVersion);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+ TEST_BROWSER_SERVICE, mConnectionCallback, new Bundle());
+ }
+ });
+
+ synchronized (mConnectionCallback.mWaitLock) {
+ mMediaBrowser.connect();
+ mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+ if (!mMediaBrowser.isConnected()) {
+ fail("Browser failed to connect!");
+ }
+ }
+ mSessionToken = mMediaBrowser.getSessionToken();
+ mController = new MediaControllerCompat(getTargetContext(), mSessionToken);
+ mController.registerCallback(mMediaControllerCallback, mHandler);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+ mMediaBrowser.disconnect();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setExtras}.
+ */
+ @Test
+ @SmallTest
+ public void testSetExtras() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ callMediaSessionMethod(SET_EXTRAS, extras, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnExtraChangedCalled);
+
+ assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+ assertBundleEquals(extras, mController.getExtras());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setFlags}.
+ */
+ @Test
+ @SmallTest
+ public void testSetFlags() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ callMediaSessionMethod(SET_FLAGS, TEST_FLAGS, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return TEST_FLAGS == mController.getFlags();
+ }
+ }.run();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setMetadata}.
+ */
+ @Test
+ @SmallTest
+ public void testSetMetadata() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ RatingCompat rating = RatingCompat.newHeartRating(true);
+ MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+ .putString(TEST_KEY, TEST_VALUE)
+ .putRating(METADATA_KEY_RATING, rating)
+ .build();
+
+ callMediaSessionMethod(SET_METADATA, metadata, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+ MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ metadataOut = mController.getMetadata();
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ assertNotNull(metadataOut.getRating(METADATA_KEY_RATING));
+ RatingCompat ratingOut = metadataOut.getRating(METADATA_KEY_RATING);
+ assertEquals(rating.getRatingStyle(), ratingOut.getRatingStyle());
+ assertEquals(rating.getPercentRating(), ratingOut.getPercentRating(), 0.0f);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setMetadata} with artwork bitmaps.
+ */
+ @Test
+ @SmallTest
+ public void testSetMetadataWithArtworks() throws Exception {
+ // TODO: Add test with a large bitmap.
+ // Using large bitmap makes other tests that are executed after this fail.
+ final Bitmap bitmapSmall = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+ .putString(TEST_KEY, TEST_VALUE)
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmapSmall)
+ .build();
+
+ callMediaSessionMethod(SET_METADATA, metadata, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+ MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ Bitmap bitmapSmallOut = metadataOut.getBitmap(MediaMetadataCompat.METADATA_KEY_ART);
+ assertNotNull(bitmapSmallOut);
+ assertEquals(bitmapSmall.getHeight(), bitmapSmallOut.getHeight());
+ assertEquals(bitmapSmall.getWidth(), bitmapSmallOut.getWidth());
+ assertEquals(bitmapSmall.getConfig(), bitmapSmallOut.getConfig());
+
+ bitmapSmallOut.recycle();
+ }
+ bitmapSmall.recycle();
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setPlaybackState}.
+ */
+ @Test
+ @SmallTest
+ public void testSetPlaybackState() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ PlaybackStateCompat state =
+ new PlaybackStateCompat.Builder()
+ .setActions(TEST_ACTION)
+ .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+ .build();
+
+ callMediaSessionMethod(SET_PLAYBACK_STATE, state, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnPlaybackStateChangedCalled);
+
+ PlaybackStateCompat stateOut = mMediaControllerCallback.mPlaybackState;
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+ assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+
+ stateOut = mController.getPlaybackState();
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+ assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setQueue} and {@link MediaSessionCompat#setQueueTitle}.
+ */
+ @Test
+ @SmallTest
+ public void testSetQueueAndSetQueueTitle() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ List<QueueItem> queue = new ArrayList<>();
+
+ MediaDescriptionCompat description1 =
+ new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_1).build();
+ MediaDescriptionCompat description2 =
+ new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_2).build();
+ QueueItem item1 = new MediaSessionCompat.QueueItem(description1, TEST_QUEUE_ID_1);
+ QueueItem item2 = new MediaSessionCompat.QueueItem(description2, TEST_QUEUE_ID_2);
+ queue.add(item1);
+ queue.add(item2);
+
+ callMediaSessionMethod(SET_QUEUE, queue, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+ callMediaSessionMethod(SET_QUEUE_TITLE, TEST_VALUE, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+ assertEquals(TEST_VALUE, mMediaControllerCallback.mTitle);
+ assertQueueEquals(queue, mMediaControllerCallback.mQueue);
+
+ assertEquals(TEST_VALUE, mController.getQueueTitle());
+ assertQueueEquals(queue, mController.getQueue());
+
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_QUEUE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+ callMediaSessionMethod(SET_QUEUE_TITLE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+ assertNull(mMediaControllerCallback.mTitle);
+ assertNull(mMediaControllerCallback.mQueue);
+ assertNull(mController.getQueueTitle());
+ assertNull(mController.getQueue());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setSessionActivity}.
+ */
+ @Test
+ @SmallTest
+ public void testSessionActivity() throws Exception {
+ synchronized (mWaitLock) {
+ Intent intent = new Intent("MEDIA_SESSION_ACTION");
+ final int requestCode = 555;
+ final PendingIntent pi =
+ PendingIntent.getActivity(getTargetContext(), requestCode, intent, 0);
+
+ callMediaSessionMethod(SET_SESSION_ACTIVITY, pi, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return pi.equals(mController.getSessionActivity());
+ }
+ }.run();
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setCaptioningEnabled}.
+ */
+ @Test
+ @SmallTest
+ public void testSetCaptioningEnabled() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_CAPTIONING_ENABLED, true, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+ assertEquals(true, mMediaControllerCallback.mCaptioningEnabled);
+ assertEquals(true, mController.isCaptioningEnabled());
+
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_CAPTIONING_ENABLED, false, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+ assertEquals(false, mMediaControllerCallback.mCaptioningEnabled);
+ assertEquals(false, mController.isCaptioningEnabled());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setRepeatMode}.
+ */
+ @Test
+ @SmallTest
+ public void testSetRepeatMode() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ callMediaSessionMethod(SET_REPEAT_MODE, repeatMode, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnRepeatModeChangedCalled);
+ assertEquals(repeatMode, mMediaControllerCallback.mRepeatMode);
+ assertEquals(repeatMode, mController.getRepeatMode());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setShuffleMode}.
+ */
+ @Test
+ @SmallTest
+ public void testSetShuffleMode() throws Exception {
+ final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(SET_SHUFFLE_MODE, shuffleMode, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnShuffleModeChangedCalled);
+ assertEquals(shuffleMode, mMediaControllerCallback.mShuffleMode);
+ assertEquals(shuffleMode, mController.getShuffleMode());
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#sendSessionEvent}.
+ */
+ @Test
+ @SmallTest
+ public void testSendSessionEvent() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+
+ Bundle arguments = new Bundle();
+ arguments.putString("event", TEST_SESSION_EVENT);
+
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ arguments.putBundle("extras", extras);
+ callMediaSessionMethod(SEND_SESSION_EVENT, arguments, getContext());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnSessionEventCalled);
+ assertEquals(TEST_SESSION_EVENT, mMediaControllerCallback.mEvent);
+ assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#release}.
+ */
+ @Test
+ @SmallTest
+ public void testRelease() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ callMediaSessionMethod(RELEASE, null, getContext());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnSessionDestroyedCalled);
+ }
+ }
+
+ /**
+ * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
+ * {@link MediaSessionCompat#setPlaybackToRemote}.
+ */
+ @LargeTest
+ public void testPlaybackToLocalAndRemote() throws Exception {
+ synchronized (mWaitLock) {
+ mMediaControllerCallback.resetLocked();
+ ParcelableVolumeInfo volumeInfo = new ParcelableVolumeInfo(
+ MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ STREAM_MUSIC,
+ VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ TEST_MAX_VOLUME,
+ TEST_CURRENT_VOLUME);
+
+ callMediaSessionMethod(SET_PLAYBACK_TO_REMOTE, volumeInfo, getContext());
+ MediaControllerCompat.PlaybackInfo info = null;
+ for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+ mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+ info = mMediaControllerCallback.mPlaybackInfo;
+ if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+ && info.getMaxVolume() == TEST_MAX_VOLUME
+ && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+ && info.getPlaybackType()
+ == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ break;
+ }
+ }
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ info.getVolumeControl());
+
+ info = mController.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+ // test setPlaybackToLocal
+ mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+ callMediaSessionMethod(SET_PLAYBACK_TO_LOCAL, AudioManager.STREAM_RING, getContext());
+
+ // In API 21 and 22, onAudioInfoChanged is not called.
+ if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) {
+ Thread.sleep(TIME_OUT_MS);
+ } else {
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+ }
+
+ info = mController.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ info.getPlaybackType());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGetRatingType() {
+ assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+ RatingCompat.RATING_NONE, mController.getRatingType());
+
+ callMediaSessionMethod(SET_RATING_TYPE, RatingCompat.RATING_5_STARS, getContext());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ public boolean check() {
+ return RatingCompat.RATING_5_STARS == mController.getRatingType();
+ }
+ }.run();
+ }
+
+ @Test
+ @SmallTest
+ public void testSessionReady() throws Exception {
+ if (android.os.Build.VERSION.SDK_INT < 21) {
+ return;
+ }
+
+ final MediaSessionCompat.Token tokenWithoutExtraBinder =
+ MediaSessionCompat.Token.fromToken(mSessionToken.getToken());
+
+ final MediaControllerCallback callback = new MediaControllerCallback();
+ synchronized (mWaitLock) {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ MediaControllerCompat controller = new MediaControllerCompat(
+ getInstrumentation().getTargetContext(), tokenWithoutExtraBinder);
+ controller.registerCallback(callback, new Handler());
+ assertFalse(controller.isSessionReady());
+ } catch (Exception e) {
+ fail();
+ }
+ }
+ });
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(callback.mOnSessionReadyCalled);
+ }
+ }
+
+ private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
+ if (expected == null || observed == null) {
+ assertTrue(expected == observed);
+ return;
+ }
+
+ assertEquals(expected.size(), observed.size());
+ for (int i = 0; i < expected.size(); i++) {
+ QueueItem expectedItem = expected.get(i);
+ QueueItem observedItem = observed.get(i);
+
+ assertEquals(expectedItem.getQueueId(), observedItem.getQueueId());
+ assertEquals(expectedItem.getDescription().getMediaId(),
+ observedItem.getDescription().getMediaId());
+ }
+ }
+
+ private class MediaControllerCallback extends MediaControllerCompat.Callback {
+ private volatile boolean mOnPlaybackStateChangedCalled;
+ private volatile boolean mOnMetadataChangedCalled;
+ private volatile boolean mOnQueueChangedCalled;
+ private volatile boolean mOnQueueTitleChangedCalled;
+ private volatile boolean mOnExtraChangedCalled;
+ private volatile boolean mOnAudioInfoChangedCalled;
+ private volatile boolean mOnSessionDestroyedCalled;
+ private volatile boolean mOnSessionEventCalled;
+ private volatile boolean mOnCaptioningEnabledChangedCalled;
+ private volatile boolean mOnRepeatModeChangedCalled;
+ private volatile boolean mOnShuffleModeChangedCalled;
+ private volatile boolean mOnSessionReadyCalled;
+
+ private volatile PlaybackStateCompat mPlaybackState;
+ private volatile MediaMetadataCompat mMediaMetadata;
+ private volatile List<QueueItem> mQueue;
+ private volatile CharSequence mTitle;
+ private volatile String mEvent;
+ private volatile Bundle mExtras;
+ private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+ private volatile boolean mCaptioningEnabled;
+ private volatile int mRepeatMode;
+ private volatile int mShuffleMode;
+
+ public void resetLocked() {
+ mOnPlaybackStateChangedCalled = false;
+ mOnMetadataChangedCalled = false;
+ mOnQueueChangedCalled = false;
+ mOnQueueTitleChangedCalled = false;
+ mOnExtraChangedCalled = false;
+ mOnAudioInfoChangedCalled = false;
+ mOnSessionDestroyedCalled = false;
+ mOnSessionEventCalled = false;
+ mOnRepeatModeChangedCalled = false;
+ mOnShuffleModeChangedCalled = false;
+
+ mPlaybackState = null;
+ mMediaMetadata = null;
+ mQueue = null;
+ mTitle = null;
+ mExtras = null;
+ mPlaybackInfo = null;
+ mCaptioningEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ synchronized (mWaitLock) {
+ mOnPlaybackStateChangedCalled = true;
+ mPlaybackState = state;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ synchronized (mWaitLock) {
+ mOnMetadataChangedCalled = true;
+ mMediaMetadata = metadata;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueChanged(List<QueueItem> queue) {
+ synchronized (mWaitLock) {
+ mOnQueueChangedCalled = true;
+ mQueue = queue;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) {
+ synchronized (mWaitLock) {
+ mOnQueueTitleChangedCalled = true;
+ mTitle = title;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnExtraChangedCalled = true;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+ synchronized (mWaitLock) {
+ mOnAudioInfoChangedCalled = true;
+ mPlaybackInfo = info;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ synchronized (mWaitLock) {
+ mOnSessionDestroyedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSessionEventCalled = true;
+ mEvent = event;
+ mExtras = (Bundle) extras.clone();
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCaptioningEnabledChanged(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnCaptioningEnabledChangedCalled = true;
+ mCaptioningEnabled = enabled;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnRepeatModeChangedCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onShuffleModeChanged(int shuffleMode) {
+ synchronized (mWaitLock) {
+ mOnShuffleModeChangedCalled = true;
+ mShuffleMode = shuffleMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionReady() {
+ synchronized (mWaitLock) {
+ mOnSessionReadyCalled = true;
+ mWaitLock.notify();
+ }
+ }
+ }
+
+ private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ final Object mWaitLock = new Object();
+
+ @Override
+ public void onConnected() {
+ synchronized (mWaitLock) {
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/service/AndroidManifest.xml b/media-compat/version-compat-tests/previous/service/AndroidManifest.xml
new file mode 100644
index 0000000..5e25a83
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="android.support.mediacompat.service"/>
diff --git a/media-compat/version-compat-tests/previous/service/build.gradle b/media-compat/version-compat-tests/previous/service/build.gradle
new file mode 100644
index 0000000..03f95ce
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ androidTestImplementation project(':support-media-compat-test-lib')
+ androidTestImplementation "com.android.support:support-media-compat:27.0.1"
+
+ androidTestImplementation(libs.test_runner)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+}
+
+supportLibrary {
+ legacySourceLocation = true
+}
diff --git a/media-compat/version-compat-tests/previous/service/lint-baseline.xml b/media-compat/version-compat-tests/previous/service/lint-baseline.xml
new file mode 100644
index 0000000..ed7ade1
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/lint-baseline.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml b/media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml
new file mode 100644
index 0000000..b47eecf
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.mediacompat.service.test">
+ <application>
+ <receiver android:name="android.support.mediacompat.service.ServiceBroadcastReceiver">
+ <intent-filter>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"/>
+ <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
+ <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompat">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
+
+ <service android:name="android.support.mediacompat.service.StubMediaBrowserServiceCompatWithDelayedMediaSession">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/media-compat/version-compat-tests/previous/service/tests/NO_DOCS b/media-compat/version-compat-tests/previous/service/tests/NO_DOCS
new file mode 100644
index 0000000..61c9b1a
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
new file mode 100644
index 0000000..d36eba3
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+ .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_COMMAND;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_TAG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_CLIENT_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaControllerMethod;
+import static android.support.mediacompat.testlib.util.IntentUtil.callTransportControlsMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaSessionCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaSessionCompatCallbackTest {
+
+ private static final String TAG = "MediaSessionCompatCallbackTest";
+
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final float DELTA = 1e-4f;
+ private static final boolean ENABLED = true;
+
+ private final Object mWaitLock = new Object();
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private String mClientVersion;
+ private MediaSessionCompat mSession;
+ private MediaSessionCallback mCallback = new MediaSessionCallback();
+
+ @Before
+ public void setUp() throws Exception {
+ // The version of the client app is provided through the instrumentation arguments.
+ mClientVersion = getArguments().getString(KEY_CLIENT_VERSION, "");
+ Log.d(TAG, "Client app version: " + mClientVersion);
+
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSessionCompat(getTargetContext(), TEST_SESSION_TAG);
+ mSession.setCallback(mCallback, mHandler);
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSession.release();
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCommand() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+
+ Bundle arguments = new Bundle();
+ arguments.putString("command", TEST_COMMAND);
+ Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ arguments.putBundle("extras", extras);
+ callMediaControllerMethod(
+ SEND_COMMAND, arguments, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCommandCalled);
+ assertNotNull(mCallback.mCommandCallback);
+ assertEquals(TEST_COMMAND, mCallback.mCommand);
+ assertBundleEquals(extras, mCallback.mExtras);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testAddRemoveQueueItems() throws Exception {
+ final String mediaId1 = "media_id_1";
+ final String mediaTitle1 = "media_title_1";
+ MediaDescriptionCompat itemDescription1 = new MediaDescriptionCompat.Builder()
+ .setMediaId(mediaId1).setTitle(mediaTitle1).build();
+
+ final String mediaId2 = "media_id_2";
+ final String mediaTitle2 = "media_title_2";
+ MediaDescriptionCompat itemDescription2 = new MediaDescriptionCompat.Builder()
+ .setMediaId(mediaId2).setTitle(mediaTitle2).build();
+
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ callMediaControllerMethod(
+ ADD_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnAddQueueItemCalled);
+ assertEquals(-1, mCallback.mQueueIndex);
+ assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+
+ mCallback.reset();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable("description", itemDescription2);
+ arguments.putInt("index", 0);
+ callMediaControllerMethod(
+ ADD_QUEUE_ITEM_WITH_INDEX, arguments, getContext(), mSession.getSessionToken());
+
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnAddQueueItemAtCalled);
+ assertEquals(0, mCallback.mQueueIndex);
+ assertEquals(mediaId2, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle2, mCallback.mQueueDescription.getTitle());
+
+ mCallback.reset();
+ callMediaControllerMethod(
+ REMOVE_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRemoveQueueItemCalled);
+ assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+ assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testTransportControlsAndMediaSessionCallback() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ callTransportControlsMethod(PLAY, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(PAUSE, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPauseCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(STOP, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnStopCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ FAST_FORWARD, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnFastForwardCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(REWIND, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRewindCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SKIP_TO_PREVIOUS, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SKIP_TO_NEXT, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToNextCalled);
+
+ mCallback.reset();
+ final long seekPosition = 1000;
+ callTransportControlsMethod(
+ SEEK_TO, seekPosition, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSeekToCalled);
+ assertEquals(seekPosition, mCallback.mSeekPosition);
+
+ mCallback.reset();
+ final RatingCompat rating =
+ RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+ callTransportControlsMethod(
+ SET_RATING, rating, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRatingCalled);
+ assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+ assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+ mCallback.reset();
+ final String mediaId = "test-media-id";
+ final Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ Bundle arguments = new Bundle();
+ arguments.putString("mediaId", mediaId);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final String query = "test-query";
+ arguments = new Bundle();
+ arguments.putString("query", query);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final Uri uri = Uri.parse("content://test/popcorn.mod");
+ arguments = new Bundle();
+ arguments.putParcelable("uri", uri);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PLAY_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final String action = "test-action";
+ arguments = new Bundle();
+ arguments.putString("action", action);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ SEND_CUSTOM_ACTION, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ mCallback.mOnCustomActionCalled = false;
+ final PlaybackStateCompat.CustomAction customAction =
+ new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+ .setExtras(extras)
+ .build();
+ arguments = new Bundle();
+ arguments.putParcelable("action", customAction);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ SEND_CUSTOM_ACTION_PARCELABLE,
+ arguments,
+ getContext(),
+ mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ final long queueItemId = 1000;
+ callTransportControlsMethod(
+ SKIP_TO_QUEUE_ITEM, queueItemId, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToQueueItemCalled);
+ assertEquals(queueItemId, mCallback.mQueueItemId);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ PREPARE, null, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareCalled);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putString("mediaId", mediaId);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putString("query", query);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ arguments = new Bundle();
+ arguments.putParcelable("uri", uri);
+ arguments.putBundle("extras", extras);
+ callTransportControlsMethod(
+ PREPARE_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertBundleEquals(extras, mCallback.mExtras);
+
+ mCallback.reset();
+ callTransportControlsMethod(
+ SET_CAPTIONING_ENABLED, ENABLED, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetCaptioningEnabledCalled);
+ assertEquals(ENABLED, mCallback.mCaptioningEnabled);
+
+ mCallback.reset();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ callTransportControlsMethod(
+ SET_REPEAT_MODE, repeatMode, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(repeatMode, mCallback.mRepeatMode);
+
+ mCallback.reset();
+ final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+ callTransportControlsMethod(
+ SET_SHUFFLE_MODE, shuffleMode, getContext(), mSession.getSessionToken());
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetShuffleModeCalled);
+ assertEquals(shuffleMode, mCallback.mShuffleMode);
+ }
+ }
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private long mSeekPosition;
+ private long mQueueItemId;
+ private RatingCompat mRating;
+ private String mMediaId;
+ private String mQuery;
+ private Uri mUri;
+ private String mAction;
+ private String mCommand;
+ private Bundle mExtras;
+ private ResultReceiver mCommandCallback;
+ private boolean mCaptioningEnabled;
+ private int mRepeatMode;
+ private int mShuffleMode;
+ private int mQueueIndex;
+ private MediaDescriptionCompat mQueueDescription;
+ private List<MediaSessionCompat.QueueItem> mQueue = new ArrayList<>();
+
+ private boolean mOnPlayCalled;
+ private boolean mOnPauseCalled;
+ private boolean mOnStopCalled;
+ private boolean mOnFastForwardCalled;
+ private boolean mOnRewindCalled;
+ private boolean mOnSkipToPreviousCalled;
+ private boolean mOnSkipToNextCalled;
+ private boolean mOnSeekToCalled;
+ private boolean mOnSkipToQueueItemCalled;
+ private boolean mOnSetRatingCalled;
+ private boolean mOnPlayFromMediaIdCalled;
+ private boolean mOnPlayFromSearchCalled;
+ private boolean mOnPlayFromUriCalled;
+ private boolean mOnCustomActionCalled;
+ private boolean mOnCommandCalled;
+ private boolean mOnPrepareCalled;
+ private boolean mOnPrepareFromMediaIdCalled;
+ private boolean mOnPrepareFromSearchCalled;
+ private boolean mOnPrepareFromUriCalled;
+ private boolean mOnSetCaptioningEnabledCalled;
+ private boolean mOnSetRepeatModeCalled;
+ private boolean mOnSetShuffleModeCalled;
+ private boolean mOnAddQueueItemCalled;
+ private boolean mOnAddQueueItemAtCalled;
+ private boolean mOnRemoveQueueItemCalled;
+
+ public void reset() {
+ mSeekPosition = -1;
+ mQueueItemId = -1;
+ mRating = null;
+ mMediaId = null;
+ mQuery = null;
+ mUri = null;
+ mAction = null;
+ mExtras = null;
+ mCommand = null;
+ mCommandCallback = null;
+ mCaptioningEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+ mQueueIndex = -1;
+ mQueueDescription = null;
+
+ mOnPlayCalled = false;
+ mOnPauseCalled = false;
+ mOnStopCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSkipToPreviousCalled = false;
+ mOnSkipToNextCalled = false;
+ mOnSkipToQueueItemCalled = false;
+ mOnSeekToCalled = false;
+ mOnSetRatingCalled = false;
+ mOnPlayFromMediaIdCalled = false;
+ mOnPlayFromSearchCalled = false;
+ mOnPlayFromUriCalled = false;
+ mOnCustomActionCalled = false;
+ mOnCommandCalled = false;
+ mOnPrepareCalled = false;
+ mOnPrepareFromMediaIdCalled = false;
+ mOnPrepareFromSearchCalled = false;
+ mOnPrepareFromUriCalled = false;
+ mOnSetCaptioningEnabledCalled = false;
+ mOnSetRepeatModeCalled = false;
+ mOnSetShuffleModeCalled = false;
+ mOnAddQueueItemCalled = false;
+ mOnAddQueueItemAtCalled = false;
+ mOnRemoveQueueItemCalled = false;
+ }
+
+ @Override
+ public void onPlay() {
+ synchronized (mWaitLock) {
+ mOnPlayCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ synchronized (mWaitLock) {
+ mOnPauseCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (mWaitLock) {
+ mOnStopCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ synchronized (mWaitLock) {
+ mOnFastForwardCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ synchronized (mWaitLock) {
+ mOnRewindCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ synchronized (mWaitLock) {
+ mOnSkipToPreviousCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ synchronized (mWaitLock) {
+ mOnSkipToNextCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ synchronized (mWaitLock) {
+ mOnSeekToCalled = true;
+ mSeekPosition = pos;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRating(RatingCompat rating) {
+ synchronized (mWaitLock) {
+ mOnSetRatingCalled = true;
+ mRating = rating;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnCustomActionCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(long id) {
+ synchronized (mWaitLock) {
+ mOnSkipToQueueItemCalled = true;
+ mQueueItemId = id;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+ synchronized (mWaitLock) {
+ mOnCommandCalled = true;
+ mCommand = command;
+ mExtras = extras;
+ mCommandCallback = cb;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepare() {
+ synchronized (mWaitLock) {
+ mOnPrepareCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description) {
+ synchronized (mWaitLock) {
+ mOnAddQueueItemCalled = true;
+ mQueueDescription = description;
+ mQueue.add(new MediaSessionCompat.QueueItem(description, mQueue.size()));
+ mSession.setQueue(mQueue);
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+ synchronized (mWaitLock) {
+ mOnAddQueueItemAtCalled = true;
+ mQueueIndex = index;
+ mQueueDescription = description;
+ mQueue.add(index, new MediaSessionCompat.QueueItem(description, mQueue.size()));
+ mSession.setQueue(mQueue);
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRemoveQueueItem(MediaDescriptionCompat description) {
+ synchronized (mWaitLock) {
+ mOnRemoveQueueItemCalled = true;
+ String mediaId = description.getMediaId();
+ for (int i = mQueue.size() - 1; i >= 0; --i) {
+ if (mediaId.equals(mQueue.get(i).getDescription().getMediaId())) {
+ mQueueDescription = mQueue.remove(i).getDescription();
+ mSession.setQueue(mQueue);
+ break;
+ }
+ }
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetCaptioningEnabled(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnSetCaptioningEnabledCalled = true;
+ mCaptioningEnabled = enabled;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ synchronized (mWaitLock) {
+ mOnSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mWaitLock.notify();
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
new file mode 100644
index 0000000..57364b7
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+ .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_ACTIVE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.util.IntentUtil
+ .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.ACTION_CALL_MEDIA_SESSION_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import java.util.List;
+
+public class ServiceBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
+ StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ switch (method) {
+ case NOTIFY_CHILDREN_CHANGED:
+ service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
+ break;
+ case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
+ service.sendDelayedNotifyChildrenChanged();
+ break;
+ case SEND_DELAYED_ITEM_LOADED:
+ service.sendDelayedItemLoaded();
+ break;
+ case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
+ service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case CUSTOM_ACTION_SEND_ERROR:
+ service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case CUSTOM_ACTION_SEND_RESULT:
+ service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case SET_SESSION_TOKEN:
+ StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
+ .callSetSessionToken();
+ break;
+ }
+ } else if (ACTION_CALL_MEDIA_SESSION_METHOD.equals(intent.getAction()) && extras != null) {
+ MediaSessionCompat session = StubMediaBrowserServiceCompat.sSession;
+ int method = extras.getInt(KEY_METHOD_ID, 0);
+
+ switch (method) {
+ case SET_EXTRAS:
+ session.setExtras(extras.getBundle(KEY_ARGUMENT));
+ break;
+ case SET_FLAGS:
+ session.setFlags(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_METADATA:
+ session.setMetadata((MediaMetadataCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_PLAYBACK_STATE:
+ session.setPlaybackState(
+ (PlaybackStateCompat) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_QUEUE:
+ List<QueueItem> items = extras.getParcelableArrayList(KEY_ARGUMENT);
+ session.setQueue(items);
+ break;
+ case SET_QUEUE_TITLE:
+ session.setQueueTitle(extras.getCharSequence(KEY_ARGUMENT));
+ break;
+ case SET_SESSION_ACTIVITY:
+ session.setSessionActivity((PendingIntent) extras.getParcelable(KEY_ARGUMENT));
+ break;
+ case SET_CAPTIONING_ENABLED:
+ session.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case SET_REPEAT_MODE:
+ session.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_SHUFFLE_MODE:
+ session.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SEND_SESSION_EVENT:
+ Bundle arguments = extras.getBundle(KEY_ARGUMENT);
+ session.sendSessionEvent(
+ arguments.getString("event"), arguments.getBundle("extras"));
+ break;
+ case SET_ACTIVE:
+ session.setActive(extras.getBoolean(KEY_ARGUMENT));
+ break;
+ case RELEASE:
+ session.release();
+ break;
+ case SET_PLAYBACK_TO_LOCAL:
+ session.setPlaybackToLocal(extras.getInt(KEY_ARGUMENT));
+ break;
+ case SET_PLAYBACK_TO_REMOTE:
+ ParcelableVolumeInfo volumeInfo = extras.getParcelable(KEY_ARGUMENT);
+ session.setPlaybackToRemote(new VolumeProviderCompat(
+ volumeInfo.controlType,
+ volumeInfo.maxVolume,
+ volumeInfo.currentVolume) {});
+ break;
+ case SET_RATING_TYPE:
+ session.setRatingType(RatingCompat.RATING_5_STARS);
+ break;
+ }
+ }
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
new file mode 100644
index 0000000..7032a0b
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INCLUDE_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_METADATA;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+public class StubMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
+
+ public static StubMediaBrowserServiceCompat sInstance;
+
+ public static MediaSessionCompat sSession;
+ private Bundle mExtras;
+ private Result<List<MediaItem>> mPendingLoadChildrenResult;
+ private Result<MediaItem> mPendingLoadItemResult;
+ private Bundle mPendingRootHints;
+
+ public Bundle mCustomActionExtras;
+ public Result<Bundle> mCustomActionResult;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ sInstance = this;
+ sSession = new MediaSessionCompat(this, "StubMediaBrowserServiceCompat");
+ setSessionToken(sSession.getSessionToken());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ sSession.release();
+ sSession = null;
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ mExtras = new Bundle();
+ mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+ return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
+ }
+
+ @Override
+ public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result) {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ if (MEDIA_ID_ROOT.equals(parentId)) {
+ Bundle rootHints = getBrowserRootHints();
+ for (String id : MEDIA_ID_CHILDREN) {
+ mediaItems.add(createMediaItem(id));
+ }
+ result.sendResult(mediaItems);
+ } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentId)) {
+ Assert.assertNull(mPendingLoadChildrenResult);
+ mPendingLoadChildrenResult = result;
+ mPendingRootHints = getBrowserRootHints();
+ result.detach();
+ } else if (MEDIA_ID_INVALID.equals(parentId)) {
+ result.sendResult(null);
+ }
+ }
+
+ @Override
+ public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaItem>> result,
+ @NonNull Bundle options) {
+ if (MEDIA_ID_INCLUDE_METADATA.equals(parentId)) {
+ // Test unparcelling the Bundle.
+ MediaMetadataCompat metadata = options.getParcelable(MEDIA_METADATA);
+ if (metadata == null) {
+ super.onLoadChildren(parentId, result, options);
+ } else {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ mediaItems.add(new MediaItem(metadata.getDescription(), MediaItem.FLAG_PLAYABLE));
+ result.sendResult(mediaItems);
+ }
+ } else {
+ super.onLoadChildren(parentId, result, options);
+ }
+ }
+
+ @Override
+ public void onLoadItem(String itemId, Result<MediaItem> result) {
+ if (MEDIA_ID_CHILDREN_DELAYED.equals(itemId)) {
+ mPendingLoadItemResult = result;
+ mPendingRootHints = getBrowserRootHints();
+ result.detach();
+ return;
+ }
+
+ if (MEDIA_ID_INVALID.equals(itemId)) {
+ result.sendResult(null);
+ return;
+ }
+
+ for (String id : MEDIA_ID_CHILDREN) {
+ if (id.equals(itemId)) {
+ result.sendResult(createMediaItem(id));
+ return;
+ }
+ }
+
+ // Test the case where onLoadItem is not implemented.
+ super.onLoadItem(itemId, result);
+ }
+
+ @Override
+ public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+ if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
+ result.sendResult(Collections.<MediaItem>emptyList());
+ } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
+ result.sendResult(null);
+ } else if (SEARCH_QUERY.equals(query)) {
+ List<MediaItem> items = new ArrayList<>();
+ for (String id : MEDIA_ID_CHILDREN) {
+ if (id.contains(query)) {
+ items.add(createMediaItem(id));
+ }
+ }
+ result.sendResult(items);
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
+ mCustomActionResult = result;
+ mCustomActionExtras = extras;
+ if (CUSTOM_ACTION_FOR_ERROR.equals(action)) {
+ result.sendError(null);
+ } else if (CUSTOM_ACTION.equals(action)) {
+ result.detach();
+ }
+ }
+
+ public void sendDelayedNotifyChildrenChanged() {
+ if (mPendingLoadChildrenResult != null) {
+ mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
+ mPendingRootHints = null;
+ mPendingLoadChildrenResult = null;
+ }
+ }
+
+ public void sendDelayedItemLoaded() {
+ if (mPendingLoadItemResult != null) {
+ mPendingLoadItemResult.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId(MEDIA_ID_CHILDREN_DELAYED).setExtras(mPendingRootHints).build(),
+ MediaItem.FLAG_BROWSABLE));
+ mPendingRootHints = null;
+ mPendingLoadItemResult = null;
+ }
+ }
+
+ private MediaItem createMediaItem(String id) {
+ return new MediaItem(new MediaDescriptionCompat.Builder().setMediaId(id).build(),
+ MediaItem.FLAG_BROWSABLE);
+ }
+}
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
new file mode 100644
index 0000000..509e13f
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.service;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import java.util.List;
+
+/**
+ * Stub implementation of {@link MediaBrowserServiceCompat}.
+ * This implementation does not call
+ * {@link MediaBrowserServiceCompat#setSessionToken(MediaSessionCompat.Token)} in its
+ * {@link android.app.Service#onCreate}.
+ */
+public class StubMediaBrowserServiceCompatWithDelayedMediaSession extends
+ MediaBrowserServiceCompat {
+
+ static StubMediaBrowserServiceCompatWithDelayedMediaSession sInstance;
+ private MediaSessionCompat mSession;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ sInstance = this;
+ mSession = new MediaSessionCompat(
+ this, "StubMediaBrowserServiceCompatWithDelayedMediaSession");
+ }
+
+ @Nullable
+ @Override
+ public BrowserRoot onGetRoot(@NonNull String clientPackageName,
+ int clientUid, @Nullable Bundle rootHints) {
+ return new BrowserRoot("StubRootId", null);
+ }
+
+ @Override
+ public void onLoadChildren(@NonNull String parentId,
+ @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+ result.detach();
+ }
+
+ public void callSetSessionToken() {
+ setSessionToken(mSession.getSessionToken());
+ }
+}
diff --git a/media-compat/version-compat-tests/runtest.sh b/media-compat/version-compat-tests/runtest.sh
new file mode 100755
index 0000000..d1a3c3a
--- /dev/null
+++ b/media-compat/version-compat-tests/runtest.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A script that runs media-compat-test between different versions.
+#
+# Preconditions:
+# - Exactly one test device should be connected.
+#
+# TODO:
+# - The test result should be easily seen. (Can we report the results to the Sponge?)
+# - Run specific combination of the test (e.g. Only want to test ToT-ToT)
+# - Run specific test class / method by using argument.
+# - Support simultaneous multiple device connection
+
+# Usage './runtest.sh'
+
+CLIENT_MODULE_NAME_BASE="support-media-compat-test-client"
+SERVICE_MODULE_NAME_BASE="support-media-compat-test-service"
+CLIENT_VERSION=""
+SERVICE_VERSION=""
+
+function runTest() {
+ echo "Running test: Client-$CLIENT_VERSION / Service-$SERVICE_VERSION"
+
+ local CLIENT_MODULE_NAME="$CLIENT_MODULE_NAME_BASE$([ "$CLIENT_VERSION" = "tot" ] || echo "-previous")"
+ local SERVICE_MODULE_NAME="$SERVICE_MODULE_NAME_BASE$([ "$SERVICE_VERSION" = "tot" ] || echo "-previous")"
+
+ # Build test apks
+ ./gradlew $CLIENT_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+ ./gradlew $SERVICE_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+
+ # Install the apks
+ adb install -r -d "../../out/dist/$CLIENT_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+ adb install -r -d "../../out/dist/$SERVICE_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+
+ # Run the tests
+ echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Started: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<"
+ adb shell am instrument -w -r -e package android.support.mediacompat.client -e debug false -e client_version $CLIENT_VERSION \
+ -e service_version $SERVICE_VERSION android.support.mediacompat.client.test/android.support.test.runner.AndroidJUnitRunner
+ adb shell am instrument -w -r -e package android.support.mediacompat.service -e debug false -e client_version $CLIENT_VERSION \
+ -e service_version $SERVICE_VERSION android.support.mediacompat.service.test/android.support.test.runner.AndroidJUnitRunner
+ echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Ended: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<<<"
+}
+
+
+OLD_PWD=$(pwd)
+if [[ $OLD_PWD != *"frameworks/support"* ]]; then
+ echo "Current working directory is" $OLD_PWD.
+ echo "Please re-run this script in any folder under frameworks/support."
+ exit 1;
+else
+ # Change working directory to frameworks/support
+ cd "$(echo $OLD_PWD | awk -F'frameworks/support' '{print $1}')"/frameworks/support
+fi
+
+echo "Choose the support library versions of the test you want to run:"
+echo " 1. Client-ToT / Service-ToT"
+echo " 2. Client-ToT / Service-Latest release"
+echo " 3. Client-Latest release / Service-ToT"
+echo " 4. Run all of the above"
+printf "Pick one of them: "
+
+read ANSWER
+case $ANSWER in
+ 1)
+ CLIENT_VERSION="tot"
+ SERVICE_VERSION="tot"
+ runTest
+ ;;
+ 2)
+ CLIENT_VERSION="tot"
+ SERVICE_VERSION="previous"
+ runTest
+ ;;
+ 3)
+ CLIENT_VERSION="previous"
+ SERVICE_VERSION="tot"
+ runTest
+ ;;
+ 4)
+ CLIENT_VERSION="tot"
+ SERVICE_VERSION="tot"
+ runTest
+
+ CLIENT_VERSION="tot"
+ SERVICE_VERSION="previous"
+ runTest
+
+ CLIENT_VERSION="previous"
+ SERVICE_VERSION="tot"
+ runTest
+ ;;
+esac
diff --git a/v17/preference-leanback/Android.mk b/preference-leanback/Android.mk
similarity index 100%
rename from v17/preference-leanback/Android.mk
rename to preference-leanback/Android.mk
diff --git a/v17/preference-leanback/AndroidManifest.xml b/preference-leanback/AndroidManifest.xml
similarity index 100%
rename from v17/preference-leanback/AndroidManifest.xml
rename to preference-leanback/AndroidManifest.xml
diff --git a/v17/preference-leanback/OWNERS b/preference-leanback/OWNERS
similarity index 100%
rename from v17/preference-leanback/OWNERS
rename to preference-leanback/OWNERS
diff --git a/v17/preference-leanback/api/26.0.0.txt b/preference-leanback/api/26.0.0.txt
similarity index 100%
rename from v17/preference-leanback/api/26.0.0.txt
rename to preference-leanback/api/26.0.0.txt
diff --git a/v17/preference-leanback/api/26.1.0.txt b/preference-leanback/api/26.1.0.txt
similarity index 100%
rename from v17/preference-leanback/api/26.1.0.txt
rename to preference-leanback/api/26.1.0.txt
diff --git a/v17/preference-leanback/api/27.0.0.txt b/preference-leanback/api/27.0.0.txt
similarity index 100%
rename from v17/preference-leanback/api/27.0.0.txt
rename to preference-leanback/api/27.0.0.txt
diff --git a/v17/preference-leanback/api/current.txt b/preference-leanback/api/current.txt
similarity index 100%
rename from v17/preference-leanback/api/current.txt
rename to preference-leanback/api/current.txt
diff --git a/v17/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java b/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
similarity index 100%
rename from v17/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
rename to preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
diff --git a/v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java b/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
similarity index 100%
rename from v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
rename to preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
diff --git a/v17/preference-leanback/build.gradle b/preference-leanback/build.gradle
similarity index 100%
rename from v17/preference-leanback/build.gradle
rename to preference-leanback/build.gradle
diff --git a/v17/preference-leanback/lint-baseline.xml b/preference-leanback/lint-baseline.xml
similarity index 100%
rename from v17/preference-leanback/lint-baseline.xml
rename to preference-leanback/lint-baseline.xml
diff --git a/v17/preference-leanback/res/color/lb_preference_item_primary_text_color.xml b/preference-leanback/res/color/lb_preference_item_primary_text_color.xml
similarity index 100%
rename from v17/preference-leanback/res/color/lb_preference_item_primary_text_color.xml
rename to preference-leanback/res/color/lb_preference_item_primary_text_color.xml
diff --git a/v17/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml b/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
similarity index 100%
rename from v17/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
rename to preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
diff --git a/v17/preference-leanback/res/layout-v21/leanback_preference_category.xml b/preference-leanback/res/layout-v21/leanback_preference_category.xml
similarity index 100%
rename from v17/preference-leanback/res/layout-v21/leanback_preference_category.xml
rename to preference-leanback/res/layout-v21/leanback_preference_category.xml
diff --git a/v17/preference-leanback/res/layout-v21/leanback_settings_fragment.xml b/preference-leanback/res/layout-v21/leanback_settings_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout-v21/leanback_settings_fragment.xml
rename to preference-leanback/res/layout-v21/leanback_settings_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_fragment.xml b/preference-leanback/res/layout/leanback_list_preference_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_fragment.xml
rename to preference-leanback/res/layout/leanback_list_preference_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_item_multi.xml b/preference-leanback/res/layout/leanback_list_preference_item_multi.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_item_multi.xml
rename to preference-leanback/res/layout/leanback_list_preference_item_multi.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_item_single.xml b/preference-leanback/res/layout/leanback_list_preference_item_single.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_item_single.xml
rename to preference-leanback/res/layout/leanback_list_preference_item_single.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference.xml b/preference-leanback/res/layout/leanback_preference.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference.xml
rename to preference-leanback/res/layout/leanback_preference.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_category.xml b/preference-leanback/res/layout/leanback_preference_category.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_category.xml
rename to preference-leanback/res/layout/leanback_preference_category.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_fragment.xml b/preference-leanback/res/layout/leanback_preference_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_fragment.xml
rename to preference-leanback/res/layout/leanback_preference_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_information.xml b/preference-leanback/res/layout/leanback_preference_information.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_information.xml
rename to preference-leanback/res/layout/leanback_preference_information.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml b/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
rename to preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preferences_list.xml b/preference-leanback/res/layout/leanback_preferences_list.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preferences_list.xml
rename to preference-leanback/res/layout/leanback_preferences_list.xml
diff --git a/v17/preference-leanback/res/layout/leanback_settings_fragment.xml b/preference-leanback/res/layout/leanback_settings_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_settings_fragment.xml
rename to preference-leanback/res/layout/leanback_settings_fragment.xml
diff --git a/v17/preference-leanback/res/values/colors.xml b/preference-leanback/res/values/colors.xml
similarity index 100%
rename from v17/preference-leanback/res/values/colors.xml
rename to preference-leanback/res/values/colors.xml
diff --git a/v17/preference-leanback/res/values/dimens.xml b/preference-leanback/res/values/dimens.xml
similarity index 100%
rename from v17/preference-leanback/res/values/dimens.xml
rename to preference-leanback/res/values/dimens.xml
diff --git a/v17/preference-leanback/res/values/styles.xml b/preference-leanback/res/values/styles.xml
similarity index 100%
rename from v17/preference-leanback/res/values/styles.xml
rename to preference-leanback/res/values/styles.xml
diff --git a/v17/preference-leanback/res/values/themes.xml b/preference-leanback/res/values/themes.xml
similarity index 100%
rename from v17/preference-leanback/res/values/themes.xml
rename to preference-leanback/res/values/themes.xml
diff --git a/v17/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java b/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
rename to preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java b/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
diff --git a/recyclerview-selection/Android.mk b/recyclerview-selection/Android.mk
new file mode 100644
index 0000000..ed93fa2
--- /dev/null
+++ b/recyclerview-selection/Android.mk
@@ -0,0 +1,30 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE := android-support-recyclerview-selection
+LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
+LOCAL_SRC_FILES := $(call all-java-files-under, src/main/java)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_SHARED_ANDROID_LIBRARIES := \
+ android-support-v7-recyclerview \
+ android-support-compat \
+ android-support-annotations
+LOCAL_JAR_EXCLUDE_FILES := none
+LOCAL_JAVA_LANGUAGE_VERSION := 1.7
+LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/recyclerview-selection/AndroidManifest.xml b/recyclerview-selection/AndroidManifest.xml
new file mode 100644
index 0000000..320ae3a
--- /dev/null
+++ b/recyclerview-selection/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.recyclerview.selection">
+ <uses-sdk android:minSdkVersion="14" />
+</manifest>
diff --git a/recyclerview-selection/build.gradle b/recyclerview-selection/build.gradle
new file mode 100644
index 0000000..ab1ab23
--- /dev/null
+++ b/recyclerview-selection/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api project(':recyclerview-v7')
+ api project(':support-annotations')
+ api project(':support-compat')
+
+ androidTestImplementation libs.junit
+ androidTestImplementation libs.test_runner, { exclude module: 'support-annotations' }
+ androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
+ androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+ androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+}
+
+
+android {
+ defaultConfig {
+ minSdkVersion 14
+ }
+ sourceSets {
+ main.res.srcDirs 'res', 'res-public'
+ }
+}
+
+supportLibrary {
+ name 'Android RecyclerView Selection'
+ publish false
+ legacySourceLocation true
+ inceptionYear '2017'
+ description 'Library providing item selection framework for RecyclerView. Support for single and multi selection is provided.'
+}
diff --git a/media-compat-test-client/lint-baseline.xml b/recyclerview-selection/lint-baseline.xml
similarity index 100%
copy from media-compat-test-client/lint-baseline.xml
copy to recyclerview-selection/lint-baseline.xml
diff --git a/recyclerview-selection/res/drawable/selection_band_overlay.xml b/recyclerview-selection/res/drawable/selection_band_overlay.xml
new file mode 100644
index 0000000..f780178
--- /dev/null
+++ b/recyclerview-selection/res/drawable/selection_band_overlay.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="#339999ff" />
+ <stroke android:width="1dp" android:color="#44000000" />
+</shape>
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java
new file mode 100644
index 0000000..606f35a
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ActivationCallbacks<K> {
+
+ static <K> ActivationCallbacks<K> dummy() {
+ return new ActivationCallbacks<K>() {
+ @Override
+ public boolean onItemActivated(ItemDetails item, MotionEvent e) {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Called when an item is activated. An item is activitated, for example, when
+ * there is no active selection and the user double clicks an item with a
+ * pointing device like a Mouse.
+ *
+ * @param item details of the item.
+ * @param e the event associated with item.
+ * @return true if the event was handled.
+ */
+ public abstract boolean onItemActivated(ItemDetails<K> item, MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java
new file mode 100644
index 0000000..13e87bd
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Point;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Provides support for auto-scrolling a view.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class AutoScroller {
+
+ /**
+ * Resets state of the scroller. Call this when the user activity that is driving
+ * auto-scrolling is done.
+ */
+ protected abstract void reset();
+
+ /**
+ * Processes a new input location.
+ * @param location
+ */
+ protected abstract void scroll(Point location);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java
new file mode 100644
index 0000000..9a5ae47
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Provides a means of controlling when and where band selection can be initiated.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class BandPredicate {
+
+ /** @return true if band selection can be initiated in response to the {@link MotionEvent}. */
+ public abstract boolean canInitiate(MotionEvent e);
+
+ private static boolean hasSupportedLayoutManager(RecyclerView recView) {
+ RecyclerView.LayoutManager lm = recView.getLayoutManager();
+ return lm instanceof GridLayoutManager
+ || lm instanceof LinearLayoutManager;
+ }
+
+ /**
+ * Creates a new band predicate that permits initiation of band on areas
+ * of a RecyclerView that map to RecyclerView.NO_POSITION.
+ *
+ * @param recView
+ * @return
+ */
+ @SuppressWarnings("unused")
+ public static BandPredicate noPosition(RecyclerView recView) {
+ return new NoPosition(recView);
+ }
+
+ /**
+ * Creates a new band predicate that permits initiation of band
+ * anywhere doesn't correspond to a draggable region of a item.
+ *
+ * @param detailsLookup
+ * @return
+ */
+ public static BandPredicate notDraggable(
+ RecyclerView recView, ItemDetailsLookup detailsLookup) {
+ return new NotDraggable(recView, detailsLookup);
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
+ * that have {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
+ * between views.
+ */
+ private static final class NoPosition extends BandPredicate {
+
+ private final RecyclerView mRecView;
+
+ NoPosition(RecyclerView recView) {
+ checkArgument(recView != null);
+
+ mRecView = recView;
+ }
+
+ @Override
+ public boolean canInitiate(MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecView)
+ || mRecView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ View itemView = mRecView.findChildViewUnder(e.getX(), e.getY());
+ int position = itemView != null
+ ? mRecView.getChildAdapterPosition(itemView)
+ : RecyclerView.NO_POSITION;
+
+ return position == RecyclerView.NO_POSITION;
+ }
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection in any area that is not
+ * draggable as determined by consulting
+ * {@link ItemDetailsLookup#inItemDragRegion(MotionEvent)}.
+ */
+ private static final class NotDraggable extends BandPredicate {
+
+ private final RecyclerView mRecView;
+ private final ItemDetailsLookup mDetailsLookup;
+
+ NotDraggable(RecyclerView recView, ItemDetailsLookup detailsLookup) {
+ checkArgument(recView != null);
+ checkArgument(detailsLookup != null);
+
+ mRecView = recView;
+ mDetailsLookup = detailsLookup;
+ }
+
+ @Override
+ public boolean canInitiate(MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecView)
+ || mRecView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
+ return (details == null) || !details.inDragRegion(e);
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
new file mode 100644
index 0000000..5362e2b
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
+ * instance. This class is responsible for rendering a band overlay and manipulating selection
+ * status of the items it intersects with.
+ *
+ * <p> Given the recycling nature of RecyclerView items that have scrolled off-screen would not
+ * be selectable with a band that itself was partially rendered off-screen. To address this,
+ * BandSelectionController builds a model of the list/grid information presented by RecyclerView as
+ * the user interacts with items using their pointer (and the band). Selectable items that intersect
+ * with the band, both on and off screen, are selected on pointer up.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+class BandSelectionHelper<K> implements OnItemTouchListener {
+
+ static final String TAG = "BandSelectionHelper";
+ static final boolean DEBUG = false;
+
+ private final BandHost mHost;
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final SelectionHelper<K> mSelectionHelper;
+ private final SelectionPredicate<K> mSelectionPredicate;
+ private final BandPredicate mBandPredicate;
+ private final FocusCallbacks<K> mFocusCallbacks;
+ private final ContentLock mLock;
+ private final AutoScroller mScroller;
+ private final GridModel.SelectionObserver mGridObserver;
+
+ private @Nullable Point mCurrentPosition;
+ private @Nullable Point mOrigin;
+ private @Nullable GridModel mModel;
+
+ /**
+ * See {@link BandSelectionHelper#create}.
+ */
+ BandSelectionHelper(
+ BandHost host,
+ AutoScroller scroller,
+ ItemKeyProvider<K> keyProvider,
+ SelectionHelper<K> selectionHelper,
+ SelectionPredicate<K> selectionPredicate,
+ BandPredicate bandPredicate,
+ FocusCallbacks<K> focusCallbacks,
+ ContentLock lock) {
+
+ checkArgument(host != null);
+ checkArgument(scroller != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionHelper != null);
+ checkArgument(selectionPredicate != null);
+ checkArgument(bandPredicate != null);
+ checkArgument(focusCallbacks != null);
+ checkArgument(lock != null);
+
+ mHost = host;
+ mKeyProvider = keyProvider;
+ mSelectionHelper = selectionHelper;
+ mSelectionPredicate = selectionPredicate;
+ mBandPredicate = bandPredicate;
+ mFocusCallbacks = focusCallbacks;
+ mLock = lock;
+
+ mHost.addOnScrollListener(
+ new OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ BandSelectionHelper.this.onScrolled(recyclerView, dx, dy);
+ }
+ });
+
+ mScroller = scroller;
+
+ mGridObserver = new GridModel.SelectionObserver<K>() {
+ @Override
+ public void onSelectionChanged(Set<K> updatedSelection) {
+ mSelectionHelper.setProvisionalSelection(updatedSelection);
+ }
+ };
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @return new BandSelectionHelper instance.
+ */
+ static <K> BandSelectionHelper create(
+ RecyclerView recView,
+ AutoScroller scroller,
+ @DrawableRes int bandOverlayId,
+ ItemKeyProvider<K> keyProvider,
+ SelectionHelper<K> selectionHelper,
+ SelectionPredicate<K> selectionPredicate,
+ BandPredicate bandPredicate,
+ FocusCallbacks<K> focusCallbacks,
+ ContentLock lock) {
+
+ return new BandSelectionHelper<>(
+ new DefaultBandHost<>(recView, bandOverlayId, keyProvider, selectionPredicate),
+ scroller,
+ keyProvider,
+ selectionHelper,
+ selectionPredicate,
+ bandPredicate,
+ focusCallbacks,
+ lock);
+ }
+
+ @VisibleForTesting
+ boolean isActive() {
+ boolean active = mModel != null;
+ if (DEBUG && active) {
+ mLock.checkLocked();
+ }
+ return active;
+ }
+
+ /**
+ * Clients must call reset when there are any material changes to the layout of items
+ * in RecyclerView.
+ */
+ void reset() {
+ if (!isActive()) {
+ return;
+ }
+
+ mHost.hideBand();
+ if (mModel != null) {
+ mModel.stopCapturing();
+ mModel.onDestroy();
+ }
+
+ mModel = null;
+ mOrigin = null;
+
+ mScroller.reset();
+ mLock.unblock();
+ }
+
+ @VisibleForTesting
+ boolean shouldStart(MotionEvent e) {
+ // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
+ // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
+ // mouse moves.
+ return MotionEvents.isPrimaryButtonPressed(e)
+ && MotionEvents.isActionMove(e)
+ && mBandPredicate.canInitiate(e)
+ && !isActive();
+ }
+
+ @VisibleForTesting
+ boolean shouldStop(MotionEvent e) {
+ return isActive()
+ && (MotionEvents.isActionUp(e)
+ || MotionEvents.isActionPointerUp(e)
+ || MotionEvents.isActionCancel(e));
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+ if (shouldStart(e)) {
+ startBandSelect(e);
+ } else if (shouldStop(e)) {
+ endBandSelect();
+ }
+
+ return isActive();
+ }
+
+ /**
+ * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+ */
+ @Override
+ public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+ if (shouldStop(e)) {
+ endBandSelect();
+ return;
+ }
+
+ // We shouldn't get any events in this method when band select is not active,
+ // but it turns some guests show up late to the party.
+ // Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
+ if (!isActive()) {
+ return;
+ }
+
+ if (DEBUG) {
+ checkArgument(MotionEvents.isActionMove(e));
+ checkState(mModel != null);
+ }
+
+ mCurrentPosition = MotionEvents.getOrigin(e);
+
+ mModel.resizeSelection(mCurrentPosition);
+
+ resizeBand();
+ mScroller.scroll(mCurrentPosition);
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ /**
+ * Starts band select by adding the drawable to the RecyclerView's overlay.
+ */
+ private void startBandSelect(MotionEvent e) {
+ checkState(!isActive());
+
+ if (!MotionEvents.isCtrlKeyPressed(e)) {
+ mSelectionHelper.clearSelection();
+ }
+
+ Point origin = MotionEvents.getOrigin(e);
+ if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
+
+ mModel = mHost.createGridModel();
+ mModel.addOnSelectionChangedListener(mGridObserver);
+
+ mLock.block();
+ mFocusCallbacks.clearFocus();
+ mOrigin = origin;
+ // NOTE: Pay heed that resizeBand modifies the y coordinates
+ // in onScrolled. Not sure if model expects this. If not
+ // it should be defending against this.
+ mModel.startCapturing(mOrigin);
+ }
+
+ /**
+ * Resizes the band select rectangle by using the origin and the current pointer position as
+ * two opposite corners of the selection.
+ */
+ private void resizeBand() {
+ Rect bounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
+ Math.min(mOrigin.y, mCurrentPosition.y),
+ Math.max(mOrigin.x, mCurrentPosition.x),
+ Math.max(mOrigin.y, mCurrentPosition.y));
+
+ if (VERBOSE) Log.v(TAG, "Resizing band! " + bounds);
+ mHost.showBand(bounds);
+ }
+
+ /**
+ * Ends band select by removing the overlay.
+ */
+ private void endBandSelect() {
+ if (DEBUG) {
+ Log.d(TAG, "Ending band select.");
+ checkState(mModel != null);
+ }
+
+ // TODO: Currently when a band select operation ends outside
+ // of an item (e.g. in the empty area between items),
+ // getPositionNearestOrigin may return an unselected item.
+ // Since the point of this code is to establish the
+ // anchor point for subsequent range operations (SHIFT+CLICK)
+ // we really want to do a better job figuring out the last
+ // item selected (and nearest to the cursor).
+ int firstSelected = mModel.getPositionNearestOrigin();
+ if (firstSelected != GridModel.NOT_SET
+ && mSelectionHelper.isSelected(mKeyProvider.getKey(firstSelected))) {
+ // Establish the band selection point as range anchor. This
+ // allows touch and keyboard based selection activities
+ // to be based on the band selection anchor point.
+ mSelectionHelper.anchorRange(firstSelected);
+ }
+
+ mSelectionHelper.mergeProvisionalSelection();
+ reset();
+ }
+
+ /**
+ * @see RecyclerView.OnScrollListener
+ */
+ private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!isActive()) {
+ return;
+ }
+
+ // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+ // origin remains in the same place relative to the view's items.
+ mOrigin.y -= dy;
+ resizeBand();
+ }
+
+ /**
+ * Provides functionality for BandController. Exists primarily to tests that are
+ * fully isolated from RecyclerView.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+ abstract static class BandHost<K> {
+
+ /**
+ * Returns a new GridModel instance.
+ */
+ abstract GridModel<K> createGridModel();
+
+ /**
+ * Show the band covering the bounds.
+ *
+ * @param bounds The boundaries of the band to show.
+ */
+ abstract void showBand(Rect bounds);
+
+ /**
+ * Hide the band.
+ */
+ abstract void hideBand();
+
+ /**
+ * Add a listener to be notified on scroll events.
+ *
+ * @param listener
+ */
+ abstract void addOnScrollListener(RecyclerView.OnScrollListener listener);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java
new file mode 100644
index 0000000..6891eab
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.content.Loader;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.util.Log;
+
+/**
+ * ContentLock provides a mechanism to block content from reloading while selection
+ * activities like gesture and band selection are active. Clients using live data
+ * (data loaded, for example by a {@link Loader}), should route calls to load
+ * content through this lock using {@link ContentLock#runWhenUnlocked(Runnable)}.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class ContentLock {
+
+ private static final String TAG = "ContentLock";
+
+ private int mLocks = 0;
+ private @Nullable Runnable mCallback;
+
+ /**
+ * Increment the block count by 1
+ */
+ @MainThread
+ synchronized void block() {
+ mLocks++;
+ if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mLocks + ".");
+ }
+
+ /**
+ * Decrement the block count by 1; If no other object is trying to block and there exists some
+ * callback, that callback will be run
+ */
+ @MainThread
+ synchronized void unblock() {
+ checkState(mLocks > 0);
+
+ mLocks--;
+ if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mLocks + ".");
+
+ if (mLocks == 0 && mCallback != null) {
+ mCallback.run();
+ mCallback = null;
+ }
+ }
+
+ /**
+ * Attempts to run the given Runnable if not-locked, or else the Runnable is set to be ran next
+ * (replacing any previous set Runnables).
+ */
+ @SuppressWarnings("unused")
+ public synchronized void runWhenUnlocked(Runnable runnable) {
+ if (mLocks == 0) {
+ runnable.run();
+ } else {
+ mCallback = runnable;
+ }
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is locked.
+ */
+ void checkLocked() {
+ checkState(mLocks > 0);
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is unlocked.
+ */
+ void checkUnlocked() {
+ checkState(mLocks == 0);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
new file mode 100644
index 0000000..f0fd4fe
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.view.View;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * RecyclerView backed {@link BandSelectionHelper.BandHost}.
+ */
+final class DefaultBandHost<K> extends GridModel.GridHost<K> {
+
+ private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
+
+ private final RecyclerView mRecView;
+ private final Drawable mBand;
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final SelectionPredicate<K> mSelectionPredicate;
+
+ DefaultBandHost(
+ RecyclerView recView,
+ @DrawableRes int bandOverlayId,
+ ItemKeyProvider<K> keyProvider,
+ SelectionPredicate<K> selectionPredicate) {
+
+ checkArgument(recView != null);
+
+ mRecView = recView;
+ mBand = mRecView.getContext().getResources().getDrawable(bandOverlayId);
+
+ checkArgument(mBand != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+
+ mRecView.addItemDecoration(
+ new ItemDecoration() {
+ @Override
+ public void onDrawOver(
+ Canvas canvas,
+ RecyclerView unusedParent,
+ RecyclerView.State unusedState) {
+ DefaultBandHost.this.onDrawBand(canvas);
+ }
+ });
+ }
+
+ @Override
+ GridModel<K> createGridModel() {
+ return new GridModel<>(this, mKeyProvider, mSelectionPredicate);
+ }
+
+ @Override
+ int getAdapterPositionAt(int index) {
+ return mRecView.getChildAdapterPosition(mRecView.getChildAt(index));
+ }
+
+ @Override
+ void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+ mRecView.addOnScrollListener(listener);
+ }
+
+ @Override
+ void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+ mRecView.removeOnScrollListener(listener);
+ }
+
+ @Override
+ Point createAbsolutePoint(Point relativePoint) {
+ return new Point(relativePoint.x + mRecView.computeHorizontalScrollOffset(),
+ relativePoint.y + mRecView.computeVerticalScrollOffset());
+ }
+
+ @Override
+ Rect getAbsoluteRectForChildViewAt(int index) {
+ final View child = mRecView.getChildAt(index);
+ final Rect childRect = new Rect();
+ child.getHitRect(childRect);
+ childRect.left += mRecView.computeHorizontalScrollOffset();
+ childRect.right += mRecView.computeHorizontalScrollOffset();
+ childRect.top += mRecView.computeVerticalScrollOffset();
+ childRect.bottom += mRecView.computeVerticalScrollOffset();
+ return childRect;
+ }
+
+ @Override
+ int getVisibleChildCount() {
+ return mRecView.getChildCount();
+ }
+
+ @Override
+ int getColumnCount() {
+ RecyclerView.LayoutManager layoutManager = mRecView.getLayoutManager();
+ if (layoutManager instanceof GridLayoutManager) {
+ return ((GridLayoutManager) layoutManager).getSpanCount();
+ }
+
+ // Otherwise, it is a list with 1 column.
+ return 1;
+ }
+
+ @Override
+ void showBand(Rect rect) {
+ mBand.setBounds(rect);
+ // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
+ // NOTE: That without invalidating rv, the band only gets updated
+ // when the pointer moves off a the item view into "NO_POSITION" territory.
+ mRecView.invalidate();
+ }
+
+ @Override
+ void hideBand() {
+ mBand.setBounds(NILL_RECT);
+ // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
+ mRecView.invalidate();
+ }
+
+ private void onDrawBand(Canvas c) {
+ mBand.draw(c);
+ }
+
+ @Override
+ boolean hasView(int pos) {
+ return mRecView.findViewHolderForAdapterPosition(pos) != null;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java
new file mode 100644
index 0000000..5625e3d
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import androidx.recyclerview.selection.Range.RangeType;
+
+/**
+ * {@link SelectionHelper} providing support for traditional multi-item selection on top
+ * of {@link RecyclerView}.
+ *
+ * <p>The class supports running in a single-select mode, which can be enabled
+ * by passing {@code #MODE_SINGLE} to the constructor.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
+
+ private static final String TAG = "DefaultSelectionHelper";
+
+ private final Selection<K> mSelection = new Selection<>();
+ private final List<SelectionObserver> mObservers = new ArrayList<>(1);
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final SelectionPredicate<K> mSelectionPredicate;
+ private final RangeCallbacks mRangeCallbacks;
+ private final boolean mSingleSelect;
+
+ private @Nullable Range mRange;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param keyProvider client supplied class providing access to stable ids.
+ * @param selectionPredicate A predicate allowing the client to disallow selection
+ * of individual elements.
+ */
+ public DefaultSelectionHelper(
+ ItemKeyProvider keyProvider,
+ SelectionPredicate selectionPredicate) {
+
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+ mRangeCallbacks = new RangeCallbacks();
+
+ mSingleSelect = !selectionPredicate.canSelectMultiple();
+ }
+
+ @Override
+ public void addObserver(SelectionObserver callback) {
+ checkArgument(callback != null);
+ mObservers.add(callback);
+ }
+
+ @Override
+ public boolean hasSelection() {
+ return !mSelection.isEmpty();
+ }
+
+ @Override
+ public Selection getSelection() {
+ return mSelection;
+ }
+
+ @Override
+ public void copySelection(Selection dest) {
+ dest.copyFrom(mSelection);
+ }
+
+ @Override
+ public boolean isSelected(@Nullable K key) {
+ return mSelection.contains(key);
+ }
+
+ @Override
+ public void restoreSelection(Selection other) {
+ checkArgument(other != null);
+ setItemsSelectedQuietly(other.mSelection, true);
+ // NOTE: We intentionally don't restore provisional selection. It's provisional.
+ notifySelectionRestored();
+ }
+
+ @Override
+ public boolean setItemsSelected(Iterable<K> keys, boolean selected) {
+ boolean changed = setItemsSelectedQuietly(keys, selected);
+ notifySelectionChanged();
+ return changed;
+ }
+
+ private boolean setItemsSelectedQuietly(Iterable<K> keys, boolean selected) {
+ boolean changed = false;
+ for (K key: keys) {
+ boolean itemChanged = selected
+ ? canSetState(key, true) && mSelection.add(key)
+ : canSetState(key, false) && mSelection.remove(key);
+ if (itemChanged) {
+ notifyItemStateChanged(key, selected);
+ }
+ changed |= itemChanged;
+ }
+ return changed;
+ }
+
+ @Override
+ public void clearSelection() {
+ if (!hasSelection()) {
+ return;
+ }
+
+ Selection prev = clearSelectionQuietly();
+ notifySelectionCleared(prev);
+ notifySelectionChanged();
+ }
+
+ @Override
+ public boolean clear() {
+ boolean somethingChanged = hasSelection();
+ clearProvisionalSelection();
+ clearSelection();
+ return somethingChanged;
+ }
+
+ /**
+ * Clears the selection, without notifying selection listeners.
+ * Returns items in previous selection. Callers are responsible for notifying
+ * listeners about changes.
+ */
+ private Selection clearSelectionQuietly() {
+ mRange = null;
+
+ Selection prevSelection = new Selection();
+ if (hasSelection()) {
+ copySelection(prevSelection);
+ mSelection.clear();
+ }
+
+ return prevSelection;
+ }
+
+ @Override
+ public boolean select(K key) {
+ checkArgument(key != null);
+
+ if (!mSelection.contains(key)) {
+ if (!canSetState(key, true)) {
+ if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
+ return false;
+ }
+
+ // Enforce single selection policy.
+ if (mSingleSelect && hasSelection()) {
+ Selection prev = clearSelectionQuietly();
+ notifySelectionCleared(prev);
+ }
+
+ mSelection.add(key);
+ notifyItemStateChanged(key, true);
+ notifySelectionChanged();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean deselect(K key) {
+ checkArgument(key != null);
+
+ if (mSelection.contains(key)) {
+ if (!canSetState(key, false)) {
+ if (DEBUG) Log.d(TAG, "Deselect cancelled by selection predicate test.");
+ return false;
+ }
+ mSelection.remove(key);
+ notifyItemStateChanged(key, false);
+ notifySelectionChanged();
+ if (mSelection.isEmpty() && isRangeActive()) {
+ // if there's nothing in the selection and there is an active ranger it results
+ // in unexpected behavior when the user tries to start range selection: the item
+ // which the ranger 'thinks' is the already selected anchor becomes unselectable
+ endRange();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void startRange(int position) {
+ select(mKeyProvider.getKey(position));
+ anchorRange(position);
+ }
+
+ @Override
+ public void extendRange(int position) {
+ extendRange(position, Range.TYPE_PRIMARY);
+ }
+
+ @Override
+ public void endRange() {
+ mRange = null;
+ // Clean up in case there was any leftover provisional selection
+ clearProvisionalSelection();
+ }
+
+ @Override
+ public void anchorRange(int position) {
+ checkArgument(position != RecyclerView.NO_POSITION);
+ checkArgument(mSelection.contains(mKeyProvider.getKey(position)));
+
+ mRange = new Range(position, mRangeCallbacks);
+ }
+
+ @Override
+ public void extendProvisionalRange(int position) {
+ if (mSingleSelect) {
+ return;
+ }
+
+ if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position);
+ checkState(isRangeActive(), "Range start point not set.");
+ extendRange(position, Range.TYPE_PROVISIONAL);
+ }
+
+ /**
+ * Sets the end point for the current range selection, started by a call to
+ * {@link #startRange(int)}. This function should only be called when a range selection
+ * is active (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+ * selected or in provisional select, depending on the type supplied. Note that if the type is
+ * provisional selection, one should do {@link #mergeProvisionalSelection()} at some
+ * point before calling on {@link #endRange()}.
+ *
+ * @param position The new end position for the selection range.
+ * @param type The type of selection the range should utilize.
+ */
+ private void extendRange(int position, @RangeType int type) {
+ checkState(isRangeActive(), "Range start point not set.");
+
+ mRange.extendRange(position, type);
+
+ // We're being lazy here notifying even when something might not have changed.
+ // To make this more correct, we'd need to update the Ranger class to return
+ // information about what has changed.
+ notifySelectionChanged();
+ }
+
+ @Override
+ public void setProvisionalSelection(Set<K> newSelection) {
+ if (mSingleSelect) {
+ return;
+ }
+
+ Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection);
+ for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
+ notifyItemStateChanged(entry.getKey(), entry.getValue());
+ }
+
+ notifySelectionChanged();
+ }
+
+ @Override
+ public void mergeProvisionalSelection() {
+ mSelection.mergeProvisionalSelection();
+
+ // Note, that for almost all functional purposes, merging a provisional selection
+ // into a the primary selection doesn't change the selection, just an internal
+ // representation of it. But there are some nuanced areas cases where
+ // that isn't true. equality for 1. So, we notify regardless.
+
+ notifySelectionChanged();
+ }
+
+ @Override
+ public void clearProvisionalSelection() {
+ for (K key : mSelection.mProvisionalSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ mSelection.clearProvisionalSelection();
+ }
+
+ @Override
+ public boolean isRangeActive() {
+ return mRange != null;
+ }
+
+ private boolean canSetState(K key, boolean nextState) {
+ return mSelectionPredicate.canSetStateForKey(key, nextState);
+ }
+
+ @Override
+ void onDataSetChanged() {
+ mSelection.clearProvisionalSelection();
+
+ notifySelectionReset();
+
+ for (K key : mSelection) {
+ // If the underlying data set has changed, before restoring
+ // selection we must re-verify that it can be selected.
+ // Why? Because if the dataset has changed, then maybe the
+ // selectability of an item has changed.
+ if (!canSetState(key, true)) {
+ deselect(key);
+ } else {
+ int lastListener = mObservers.size() - 1;
+ for (int i = lastListener; i >= 0; i--) {
+ mObservers.get(i).onItemStateChanged(key, true);
+ }
+ }
+ }
+
+ notifySelectionChanged();
+ }
+
+ /**
+ * Notifies registered listeners when the selection status of a single item
+ * (identified by {@code position}) changes.
+ */
+ private void notifyItemStateChanged(K key, boolean selected) {
+ checkArgument(key != null);
+
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onItemStateChanged(key, selected);
+ }
+ }
+
+ private void notifySelectionCleared(Selection<K> selection) {
+ for (K key: selection.mSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ for (K key: selection.mProvisionalSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ }
+
+ /**
+ * Notifies registered listeners when the selection has changed. This
+ * notification should be sent only once a full series of changes
+ * is complete, e.g. clearingSelection, or updating the single
+ * selection from one item to another.
+ */
+ private void notifySelectionChanged() {
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionChanged();
+ }
+ }
+
+ private void notifySelectionRestored() {
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionRestored();
+ }
+ }
+
+ private void notifySelectionReset() {
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionReset();
+ }
+ }
+
+ private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
+ switch (type) {
+ case Range.TYPE_PRIMARY:
+ updateForRegularRange(begin, end, selected);
+ break;
+ case Range.TYPE_PROVISIONAL:
+ updateForProvisionalRange(begin, end, selected);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid range type: " + type);
+ }
+ }
+
+ private void updateForRegularRange(int begin, int end, boolean selected) {
+ checkArgument(end >= begin);
+
+ for (int i = begin; i <= end; i++) {
+ K key = mKeyProvider.getKey(i);
+ if (key == null) {
+ continue;
+ }
+
+ if (selected) {
+ select(key);
+ } else {
+ deselect(key);
+ }
+ }
+ }
+
+ private void updateForProvisionalRange(int begin, int end, boolean selected) {
+ checkArgument(end >= begin);
+
+ for (int i = begin; i <= end; i++) {
+ K key = mKeyProvider.getKey(i);
+ if (key == null) {
+ continue;
+ }
+
+ boolean changedState = false;
+ if (selected) {
+ boolean canSelect = canSetState(key, true);
+ if (canSelect && !mSelection.mSelection.contains(key)) {
+ mSelection.mProvisionalSelection.add(key);
+ changedState = true;
+ }
+ } else {
+ mSelection.mProvisionalSelection.remove(key);
+ changedState = true;
+ }
+
+ // Only notify item callbacks when something's state is actually changed in provisional
+ // selection.
+ if (changedState) {
+ notifyItemStateChanged(key, selected);
+ }
+ }
+
+ notifySelectionChanged();
+ }
+
+ private final class RangeCallbacks extends Range.Callbacks {
+ @Override
+ void updateForRange(int begin, int end, boolean selected, int type) {
+ switch (type) {
+ case Range.TYPE_PRIMARY:
+ updateForRegularRange(begin, end, selected);
+ break;
+ case Range.TYPE_PROVISIONAL:
+ updateForProvisionalRange(begin, end, selected);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid range type: " + type);
+ }
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java
new file mode 100644
index 0000000..b418ad4
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+/**
+ * Provides the necessary glue to notify RecyclerView when selection data changes,
+ * and to notify SelectionHelper when the underlying RecyclerView.Adapter data changes.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@VisibleForTesting
+public class EventBridge {
+
+ private static final String TAG = "EventsRelays";
+
+ /**
+ * Installs the event bridge for on the supplied adapter/helper.
+ *
+ * @param adapter
+ * @param selectionHelper
+ * @param keyProvider
+ * @param <K>
+ */
+ @VisibleForTesting
+ public static <K> void install(
+ RecyclerView.Adapter<?> adapter,
+ SelectionHelper<K> selectionHelper,
+ ItemKeyProvider<K> keyProvider) {
+ new AdapterToSelectionHelper(adapter, selectionHelper);
+ new SelectionHelperToAdapter<>(selectionHelper, keyProvider, adapter);
+ }
+
+ private static final class AdapterToSelectionHelper extends RecyclerView.AdapterDataObserver {
+
+ private final SelectionHelper<?> mSelectionHelper;
+
+ AdapterToSelectionHelper(
+ RecyclerView.Adapter<?> adapter,
+ SelectionHelper<?> selectionHelper) {
+ adapter.registerAdapterDataObserver(this);
+
+ checkArgument(selectionHelper != null);
+ mSelectionHelper = selectionHelper;
+ }
+
+ @Override
+ public void onChanged() {
+ mSelectionHelper.onDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int startPosition, int itemCount, Object payload) {
+ // No change in position. Ignore, since we assume
+ // selection is a user driven activity. So changes
+ // in properties of items shouldn't result in a
+ // change of selection.
+ // TODO: It is possible properties of items chould change to make them unselectable.
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ // Uninteresting to us since selection is stable ID based.
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ // Uninteresting to us since selection is stable ID based.
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ // Uninteresting to us since selection is stable ID based.
+ }
+ }
+
+ private static final class SelectionHelperToAdapter<K>
+ extends SelectionHelper.SelectionObserver<K> {
+
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final RecyclerView.Adapter<?> mAdapter;
+
+ SelectionHelperToAdapter(
+ SelectionHelper<K> selectionHelper,
+ ItemKeyProvider<K> keyProvider,
+ RecyclerView.Adapter<?> adapter) {
+
+ selectionHelper.addObserver(this);
+
+ checkArgument(keyProvider != null);
+ checkArgument(adapter != null);
+
+ mKeyProvider = keyProvider;
+ mAdapter = adapter;
+ }
+
+ /**
+ * Called when state of an item has been changed.
+ */
+ @Override
+ public void onItemStateChanged(K key, boolean selected) {
+ int position = mKeyProvider.getPosition(key);
+ if (VERBOSE) Log.v(TAG, "ITEM " + key + " CHANGED at pos: " + position);
+
+ if (position < 0) {
+ Log.w(TAG, "Item change notification received for unknown item: " + key);
+ return;
+ }
+
+ mAdapter.notifyItemChanged(position, SelectionHelper.SELECTION_CHANGED_MARKER);
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java
new file mode 100644
index 0000000..4c1c12e
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class FocusCallbacks<K> {
+
+ static final <K> FocusCallbacks<K> dummy() {
+ return new FocusCallbacks<K>() {
+ @Override
+ public void focusItem(ItemDetails<K> item) {
+ }
+
+ @Override
+ public boolean hasFocusedItem() {
+ return false;
+ }
+
+ @Override
+ public int getFocusedPosition() {
+ return RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ public void clearFocus() {
+ }
+ };
+ }
+
+ /**
+ * If environment supports focus, focus {@code item}.
+ */
+ public abstract void focusItem(ItemDetails<K> item);
+
+ /**
+ * @return true if there is a focused item.
+ */
+ public abstract boolean hasFocusedItem();
+
+ /**
+ * @return the position of the currently focused item, if any.
+ */
+ public abstract int getFocusedPosition();
+
+ /**
+ * If the environment supports focus and something is focused, unfocus it.
+ */
+ public abstract void clearFocus();
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java
new file mode 100644
index 0000000..82fab87
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.Nullable;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+/**
+ * GestureRouter is responsible for routing gestures detected by a GestureDetector
+ * to registered handlers. The primary function is to divide events by tool-type
+ * allowing handlers to cleanly implement tool-type specific policies.
+ *
+ * @param <T> listener type. Must extend OnGestureListener & OnDoubleTapListener.
+ */
+final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
+ implements OnGestureListener, OnDoubleTapListener {
+
+ private final ToolHandlerRegistry<T> mDelegates;
+
+ GestureRouter(T defaultDelegate) {
+ checkArgument(defaultDelegate != null);
+ mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+ }
+
+ GestureRouter() {
+ this((T) new SimpleOnGestureListener());
+ }
+
+ /**
+ * @param toolType
+ * @param delegate the delegate, or null to unregister.
+ */
+ public void register(int toolType, @Nullable T delegate) {
+ mDelegates.set(toolType, delegate);
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return mDelegates.get(e).onSingleTapConfirmed(e);
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mDelegates.get(e).onDoubleTap(e);
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ return mDelegates.get(e).onDoubleTapEvent(e);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return mDelegates.get(e).onDown(e);
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ mDelegates.get(e).onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mDelegates.get(e).onSingleTapUp(e);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return mDelegates.get(e2).onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ mDelegates.get(e).onLongPress(e);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return mDelegates.get(e2).onFling(e1, e2, velocityX, velocityY);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
new file mode 100644
index 0000000..2a28fc5
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.graphics.Point;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * GestureSelectionHelper provides logic that interprets a combination
+ * of motions and gestures in order to provide gesture driven selection support
+ * when used in conjunction with RecyclerView and other classes in the ReyclerView
+ * selection support package.
+ */
+final class GestureSelectionHelper implements OnItemTouchListener {
+
+ private static final String TAG = "GestureSelectionHelper";
+
+ private final SelectionHelper<?> mSelectionMgr;
+ private final AutoScroller mScroller;
+ private final ViewDelegate mView;
+ private final ContentLock mLock;
+
+ private int mLastStartedItemPos = -1;
+ private boolean mStarted = false;
+ private Point mLastInterceptedPoint;
+
+ /**
+ * See {@link #create(SelectionHelper, RecyclerView, AutoScroller, ContentLock)} for convenience
+ * method.
+ */
+ GestureSelectionHelper(
+ SelectionHelper<?> selectionHelper,
+ ViewDelegate view,
+ AutoScroller scroller,
+ ContentLock lock) {
+
+ checkArgument(selectionHelper != null);
+ checkArgument(view != null);
+ checkArgument(scroller != null);
+ checkArgument(lock != null);
+
+ mSelectionMgr = selectionHelper;
+ mView = view;
+ mScroller = scroller;
+ mLock = lock;
+ }
+
+ /**
+ * Explicitly kicks off a gesture multi-select.
+ */
+ void start() {
+ checkState(!mStarted);
+ checkState(mLastStartedItemPos > -1);
+
+ // Partner code in MotionInputHandler ensures items
+ // are selected and range established prior to
+ // start being called.
+ // Verify the truth of that statement here
+ // to make the implicit coupling less of a time bomb.
+ checkState(mSelectionMgr.isRangeActive());
+
+ mLock.checkUnlocked();
+
+ mStarted = true;
+ mLock.block();
+ }
+
+ @Override
+ /** @hide */
+ public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+ if (MotionEvents.isMouseEvent(e)) {
+ if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration.");
+ }
+
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // NOTE: Unlike events with other actions, RecyclerView eats
+ // "DOWN" events. So even if we return true here we'll
+ // never see an event w/ ACTION_DOWN passed to onTouchEvent.
+ return handleInterceptedDownEvent(e);
+ case MotionEvent.ACTION_MOVE:
+ return mStarted;
+ }
+
+ return false;
+ }
+
+ @Override
+ /** @hide */
+ public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+ checkState(mStarted);
+
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ handleMoveEvent(e);
+ break;
+ case MotionEvent.ACTION_UP:
+ handleUpEvent(e);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ handleCancelEvent(e);
+ break;
+ }
+ }
+
+ @Override
+ /** @hide */
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ // Called when an ACTION_DOWN event is intercepted.
+ // If down event happens on an item, we mark that item's position as last started.
+ private boolean handleInterceptedDownEvent(MotionEvent e) {
+ mLastStartedItemPos = mView.getItemUnder(e);
+ return mLastStartedItemPos != RecyclerView.NO_POSITION;
+ }
+
+ // Called when ACTION_UP event is to be handled.
+ // Essentially, since this means all gesture movement is over, reset everything and apply
+ // provisional selection.
+ private void handleUpEvent(MotionEvent e) {
+ mSelectionMgr.mergeProvisionalSelection();
+ endSelection();
+ if (mLastStartedItemPos > -1) {
+ mSelectionMgr.startRange(mLastStartedItemPos);
+ }
+ }
+
+ // Called when ACTION_CANCEL event is to be handled.
+ // This means this gesture selection is aborted, so reset everything and abandon provisional
+ // selection.
+ private void handleCancelEvent(MotionEvent unused) {
+ mSelectionMgr.clearProvisionalSelection();
+ endSelection();
+ }
+
+ private void endSelection() {
+ checkState(mStarted);
+
+ mLastStartedItemPos = -1;
+ mStarted = false;
+ mScroller.reset();
+ mLock.unblock();
+ }
+
+ // Call when an intercepted ACTION_MOVE event is passed down.
+ // At this point, we are sure user wants to gesture multi-select.
+ private void handleMoveEvent(MotionEvent e) {
+ mLastInterceptedPoint = MotionEvents.getOrigin(e);
+
+ int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
+ if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
+ extendSelection(lastGlidedItemPos);
+ }
+
+ mScroller.scroll(mLastInterceptedPoint);
+ }
+
+ // It's possible for events to go over the top/bottom of the RecyclerView.
+ // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
+ // correctly.
+ private static float getInboundY(float max, float y) {
+ if (y < 0f) {
+ return 0f;
+ } else if (y > max) {
+ return max;
+ }
+ return y;
+ }
+
+ /* Given the end position, select everything in-between.
+ * @param endPos The adapter position of the end item.
+ */
+ private void extendSelection(int endPos) {
+ mSelectionMgr.extendProvisionalRange(endPos);
+ }
+
+ /**
+ * Returns a new instance of GestureSelectionHelper.
+ */
+ static GestureSelectionHelper create(
+ SelectionHelper selectionMgr,
+ RecyclerView recView,
+ AutoScroller scroller,
+ ContentLock lock) {
+
+ return new GestureSelectionHelper(
+ selectionMgr,
+ new RecyclerViewDelegate(recView),
+ scroller,
+ lock);
+ }
+
+ @VisibleForTesting
+ abstract static class ViewDelegate {
+ abstract int getHeight();
+
+ abstract int getItemUnder(MotionEvent e);
+
+ abstract int getLastGlidedItemPosition(MotionEvent e);
+ }
+
+ @VisibleForTesting
+ static final class RecyclerViewDelegate extends ViewDelegate {
+
+ private final RecyclerView mRecView;
+
+ RecyclerViewDelegate(RecyclerView view) {
+ checkArgument(view != null);
+ mRecView = view;
+ }
+
+ @Override
+ int getHeight() {
+ return mRecView.getHeight();
+ }
+
+ @Override
+ int getItemUnder(MotionEvent e) {
+ View child = mRecView.findChildViewUnder(e.getX(), e.getY());
+ return child != null
+ ? mRecView.getChildAdapterPosition(child)
+ : RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ int getLastGlidedItemPosition(MotionEvent e) {
+ // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
+ // last item of the recycler view), we would want to set that as the currentItemPos
+ View lastItem = mRecView.getLayoutManager()
+ .getChildAt(mRecView.getLayoutManager().getChildCount() - 1);
+ int direction = ViewCompat.getLayoutDirection(mRecView);
+ final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
+ lastItem.getLeft(),
+ lastItem.getRight(),
+ e,
+ direction);
+
+ // Since views get attached & detached from RecyclerView,
+ // {@link LayoutManager#getChildCount} can return a different number from the actual
+ // number
+ // of items in the adapter. Using the adapter is the for sure way to get the actual last
+ // item position.
+ final float inboundY = getInboundY(mRecView.getHeight(), e.getY());
+ return (pastLastItem) ? mRecView.getAdapter().getItemCount() - 1
+ : mRecView.getChildAdapterPosition(
+ mRecView.findChildViewUnder(e.getX(), inboundY));
+ }
+
+ /*
+ * Check to see if MotionEvent if past a particular item, i.e. to the right or to the bottom
+ * of the item.
+ * For RTL, it would to be to the left or to the bottom of the item.
+ */
+ @VisibleForTesting
+ static boolean isPastLastItem(int top, int left, int right, MotionEvent e, int direction) {
+ if (direction == View.LAYOUT_DIRECTION_LTR) {
+ return e.getX() > right && e.getY() > top;
+ } else {
+ return e.getX() < left && e.getY() > top;
+ }
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java
new file mode 100644
index 0000000..4358958
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Provides a band selection item model for views within a RecyclerView. This class queries the
+ * RecyclerView to determine where its items are placed; then, once band selection is underway,
+ * it alerts listeners of which items are covered by the selections.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+final class GridModel<K> {
+
+ // Magical value indicating that a value has not been previously set. primitive null :)
+ static final int NOT_SET = -1;
+
+ // Enum values used to determine the corner at which the origin is located within the
+ private static final int UPPER = 0x00;
+ private static final int LOWER = 0x01;
+ private static final int LEFT = 0x00;
+ private static final int RIGHT = 0x02;
+ private static final int UPPER_LEFT = UPPER | LEFT;
+ private static final int UPPER_RIGHT = UPPER | RIGHT;
+ private static final int LOWER_LEFT = LOWER | LEFT;
+ private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+ private final GridHost<K> mHost;
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final SelectionPredicate<K> mSelectionPredicate;
+
+ private final List<SelectionObserver> mOnSelectionChangedListeners = new ArrayList<>();
+
+ // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
+ // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
+ // mColumns.get(5) would return an array of positions in that column. Within that array, the
+ // value for key y is the adapter position for the item whose y-offset is y.
+ private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
+
+ // List of limits along the x-axis (columns).
+ // This list is sorted from furthest left to furthest right.
+ private final List<Limits> mColumnBounds = new ArrayList<>();
+
+ // List of limits along the y-axis (rows). Note that this list only contains items which
+ // have been in the viewport.
+ private final List<Limits> mRowBounds = new ArrayList<>();
+
+ // The adapter positions which have been recorded so far.
+ private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+ // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+ // throughout the lifetime of the object.
+ private final Set<K> mSelection = new HashSet<>();
+
+ // The current pointer (in absolute positioning from the top of the view).
+ private Point mPointer;
+
+ // The bounds of the band selection.
+ private RelativePoint mRelOrigin;
+ private RelativePoint mRelPointer;
+
+ private boolean mIsActive;
+
+ // Tracks where the band select originated from. This is used to determine where selections
+ // should expand from when Shift+click is used.
+ private int mPositionNearestOrigin = NOT_SET;
+
+ private final OnScrollListener mScrollListener;
+
+ GridModel(
+ GridHost host,
+ ItemKeyProvider<K> keyProvider,
+ SelectionPredicate<K> selectionPredicate) {
+
+ checkArgument(host != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+
+ mHost = host;
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+
+ mScrollListener = new OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ GridModel.this.onScrolled(recyclerView, dx, dy);
+ }
+ };
+
+ mHost.addOnScrollListener(mScrollListener);
+ }
+
+ /**
+ * Start a band select operation at the given point.
+ *
+ * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+ * For example, if the view is scrolled to the bottom, the top-left of
+ * the
+ * viewport
+ * would have a relative origin of (0, 0), even though its absolute point
+ * has a higher
+ * y-value.
+ */
+ void startCapturing(Point relativeOrigin) {
+ recordVisibleChildren();
+ if (isEmpty()) {
+ // The selection band logic works only if there is at least one visible child.
+ return;
+ }
+
+ mIsActive = true;
+ mPointer = mHost.createAbsolutePoint(relativeOrigin);
+ mRelOrigin = createRelativePoint(mPointer);
+ mRelPointer = createRelativePoint(mPointer);
+ computeCurrentSelection();
+ notifySelectionChanged();
+ }
+
+ /**
+ * Ends the band selection.
+ */
+ void stopCapturing() {
+ mIsActive = false;
+ }
+
+ /**
+ * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+ * opposite the origin.
+ *
+ * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+ * relative to the viewport. For example, if the view is scrolled to the
+ * bottom, the
+ * top-left of the viewport would have a relative origin of (0, 0), even
+ * though its
+ * absolute point has a higher y-value.
+ */
+ @VisibleForTesting
+ void resizeSelection(Point relativePointer) {
+ mPointer = mHost.createAbsolutePoint(relativePointer);
+ updateModel();
+ }
+
+ /**
+ * @return The adapter position for the item nearest the origin corresponding to the latest
+ * band select operation, or NOT_SET if the selection did not cover any items.
+ */
+ int getPositionNearestOrigin() {
+ return mPositionNearestOrigin;
+ }
+
+ private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!mIsActive) {
+ return;
+ }
+
+ mPointer.x += dx;
+ mPointer.y += dy;
+ recordVisibleChildren();
+ updateModel();
+ }
+
+ /**
+ * Queries the view for all children and records their location metadata.
+ */
+ private void recordVisibleChildren() {
+ for (int i = 0; i < mHost.getVisibleChildCount(); i++) {
+ int adapterPosition = mHost.getAdapterPositionAt(i);
+ // Sometimes the view is not attached, as we notify the multi selection manager
+ // synchronously, while views are attached asynchronously. As a result items which
+ // are in the adapter may not actually have a corresponding view (yet).
+ if (mHost.hasView(adapterPosition)
+ && mSelectionPredicate.canSetStateAtPosition(adapterPosition, true)
+ && !mKnownPositions.get(adapterPosition)) {
+ mKnownPositions.put(adapterPosition, true);
+ recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition);
+ }
+ }
+ }
+
+ /**
+ * Checks if there are any recorded children.
+ */
+ private boolean isEmpty() {
+ return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
+ }
+
+ /**
+ * Updates the limits lists and column map with the given item metadata.
+ *
+ * @param absoluteChildRect The absolute rectangle for the child view being processed.
+ * @param adapterPosition The position of the child view being processed.
+ */
+ private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+ if (mColumnBounds.size() != mHost.getColumnCount()) {
+ // If not all x-limits have been recorded, record this one.
+ recordLimits(
+ mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+ }
+
+ recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+
+ SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
+ if (columnList == null) {
+ columnList = new SparseIntArray();
+ mColumns.put(absoluteChildRect.left, columnList);
+ }
+ columnList.put(absoluteChildRect.top, adapterPosition);
+ }
+
+ /**
+ * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+ * does not exist.
+ */
+ private void recordLimits(List<Limits> limitsList, Limits limits) {
+ int index = Collections.binarySearch(limitsList, limits);
+ if (index < 0) {
+ limitsList.add(~index, limits);
+ }
+ }
+
+ /**
+ * Handles a moved pointer; this function determines whether the pointer movement resulted
+ * in a selection change and, if it has, notifies listeners of this change.
+ */
+ private void updateModel() {
+ RelativePoint old = mRelPointer;
+ mRelPointer = createRelativePoint(mPointer);
+ if (old != null && mRelPointer.equals(old)) {
+ return;
+ }
+
+ computeCurrentSelection();
+ notifySelectionChanged();
+ }
+
+ /**
+ * Computes the currently-selected items.
+ */
+ private void computeCurrentSelection() {
+ if (areItemsCoveredByBand(mRelPointer, mRelOrigin)) {
+ updateSelection(computeBounds());
+ } else {
+ mSelection.clear();
+ mPositionNearestOrigin = NOT_SET;
+ }
+ }
+
+ /**
+ * Notifies all listeners of a selection change. Note that this function simply passes
+ * mSelection, so computeCurrentSelection() should be called before this
+ * function.
+ */
+ private void notifySelectionChanged() {
+ for (SelectionObserver listener : mOnSelectionChangedListeners) {
+ listener.onSelectionChanged(mSelection);
+ }
+ }
+
+ /**
+ * @param rect Rectangle including all covered items.
+ */
+ private void updateSelection(Rect rect) {
+ int columnStart =
+ Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
+
+ checkArgument(columnStart >= 0, "Rect doesn't intesect any known column.");
+
+ int columnEnd = columnStart;
+
+ for (int i = columnStart; i < mColumnBounds.size()
+ && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
+ columnEnd = i;
+ }
+
+ int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+ if (rowStart < 0) {
+ mPositionNearestOrigin = NOT_SET;
+ return;
+ }
+
+ int rowEnd = rowStart;
+ for (int i = rowStart; i < mRowBounds.size()
+ && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+ rowEnd = i;
+ }
+
+ updateSelection(columnStart, columnEnd, rowStart, rowEnd);
+ }
+
+ /**
+ * Computes the selection given the previously-computed start- and end-indices for each
+ * row and column.
+ */
+ private void updateSelection(
+ int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+
+ if (BandSelectionHelper.DEBUG) {
+ Log.d(BandSelectionHelper.TAG, String.format(
+ "updateSelection: %d, %d, %d, %d",
+ columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+ }
+
+ mSelection.clear();
+ for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+ SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
+ for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+ // The default return value for SparseIntArray.get is 0, which is a valid
+ // position. Use a sentry value to prevent erroneously selecting item 0.
+ final int rowKey = mRowBounds.get(row).lowerLimit;
+ int position = items.get(rowKey, NOT_SET);
+ if (position != NOT_SET) {
+ K key = mKeyProvider.getKey(position);
+ if (key != null) {
+ // The adapter inserts items for UI layout purposes that aren't
+ // associated with files. Those will have a null model ID.
+ // Don't select them.
+ if (canSelect(key)) {
+ mSelection.add(key);
+ }
+ }
+ if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+ row, rowStartIndex, rowEndIndex)) {
+ // If this is the position nearest the origin, record it now so that it
+ // can be returned by endSelection() later.
+ mPositionNearestOrigin = position;
+ }
+ }
+ }
+ }
+ }
+
+ private boolean canSelect(K key) {
+ return mSelectionPredicate.canSetStateForKey(key, true);
+ }
+
+ /**
+ * @return Returns true if the position is the nearest to the origin, or, in the case of the
+ * lower-right corner, whether it is possible that the position is the nearest to the
+ * origin. See comment below for reasoning for this special case.
+ */
+ private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+ int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+ int corner = computeCornerNearestOrigin();
+ switch (corner) {
+ case UPPER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+ case UPPER_RIGHT:
+ return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+ case LOWER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+ case LOWER_RIGHT:
+ // Note that in some cases, the last row will not have as many items as there
+ // are columns (e.g., if there are 4 items and 3 columns, the second row will
+ // only have one item in the first column). This function is invoked for each
+ // position from left to right, so return true for any position in the bottom
+ // row and only the right-most position in the bottom row will be recorded.
+ return rowIndex == rowEndIndex;
+ default:
+ throw new RuntimeException("Invalid corner type.");
+ }
+ }
+
+ /**
+ * Listener for changes in which items have been band selected.
+ */
+ public abstract static class SelectionObserver<K> {
+ abstract void onSelectionChanged(Set<K> updatedSelection);
+ }
+
+ void addOnSelectionChangedListener(SelectionObserver listener) {
+ mOnSelectionChangedListeners.add(listener);
+ }
+
+ /**
+ * Called when {@link BandSelectionHelper} is finished with a GridModel.
+ */
+ void onDestroy() {
+ mOnSelectionChangedListeners.clear();
+ // Cleanup listeners to prevent memory leaks.
+ mHost.removeOnScrollListener(mScrollListener);
+ }
+
+ /**
+ * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+ * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+ * of item columns and the top- and bottom sides of item rows so that it can be determined
+ * whether the pointer is located within the bounds of an item.
+ */
+ private static class Limits implements Comparable<Limits> {
+ public int lowerLimit;
+ public int upperLimit;
+
+ Limits(int lowerLimit, int upperLimit) {
+ this.lowerLimit = lowerLimit;
+ this.upperLimit = upperLimit;
+ }
+
+ @Override
+ public int compareTo(Limits other) {
+ return lowerLimit - other.lowerLimit;
+ }
+
+ @Override
+ public int hashCode() {
+ return lowerLimit ^ upperLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Limits)) {
+ return false;
+ }
+
+ return ((Limits) other).lowerLimit == lowerLimit
+ && ((Limits) other).upperLimit == upperLimit;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + lowerLimit + ", " + upperLimit + ")";
+ }
+ }
+
+ /**
+ * The location of a coordinate relative to items. This class represents a general area of the
+ * view as it relates to band selection rather than an explicit point. For example, two
+ * different points within an item are considered to have the same "location" because band
+ * selection originating within the item would select the same items no matter which point
+ * was used. Same goes for points between items as well as those at the very beginning or end
+ * of the view.
+ *
+ * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+ * advantage of tying the value to the Limits of items along that axis. This allows easy
+ * selection of items within those Limits as opposed to a search through every item to see if a
+ * given coordinate value falls within those Limits.
+ */
+ private static class RelativeCoordinate
+ implements Comparable<RelativeCoordinate> {
+ /**
+ * Location describing points after the last known item.
+ */
+ static final int AFTER_LAST_ITEM = 0;
+
+ /**
+ * Location describing points before the first known item.
+ */
+ static final int BEFORE_FIRST_ITEM = 1;
+
+ /**
+ * Location describing points between two items.
+ */
+ static final int BETWEEN_TWO_ITEMS = 2;
+
+ /**
+ * Location describing points within the limits of one item.
+ */
+ static final int WITHIN_LIMITS = 3;
+
+ /**
+ * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+ * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+ */
+ public final int type;
+
+ /**
+ * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+ * BETWEEN_TWO_ITEMS.
+ */
+ public Limits limitsBeforeCoordinate;
+
+ /**
+ * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+ */
+ public Limits limitsAfterCoordinate;
+
+ // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+ public Limits mFirstKnownItem;
+ // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+ public Limits mLastKnownItem;
+
+ /**
+ * @param limitsList The sorted limits list for the coordinate type. If this
+ * CoordinateLocation is an x-value, mXLimitsList should be passed;
+ * otherwise,
+ * mYLimitsList should be pased.
+ * @param value The coordinate value.
+ */
+ RelativeCoordinate(List<Limits> limitsList, int value) {
+ int index = Collections.binarySearch(limitsList, new Limits(value, value));
+
+ if (index >= 0) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(index);
+ } else if (~index == 0) {
+ this.type = BEFORE_FIRST_ITEM;
+ this.mFirstKnownItem = limitsList.get(0);
+ } else if (~index == limitsList.size()) {
+ Limits lastLimits = limitsList.get(limitsList.size() - 1);
+ if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = lastLimits;
+ } else {
+ this.type = AFTER_LAST_ITEM;
+ this.mLastKnownItem = lastLimits;
+ }
+ } else {
+ Limits limitsBeforeIndex = limitsList.get(~index - 1);
+ if (limitsBeforeIndex.lowerLimit <= value
+ && value <= limitsBeforeIndex.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ } else {
+ this.type = BETWEEN_TWO_ITEMS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ this.limitsAfterCoordinate = limitsList.get(~index);
+ }
+ }
+ }
+
+ int toComparisonValue() {
+ if (type == BEFORE_FIRST_ITEM) {
+ return mFirstKnownItem.lowerLimit - 1;
+ } else if (type == AFTER_LAST_ITEM) {
+ return mLastKnownItem.upperLimit + 1;
+ } else if (type == BETWEEN_TWO_ITEMS) {
+ return limitsBeforeCoordinate.upperLimit + 1;
+ } else {
+ return limitsBeforeCoordinate.lowerLimit;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return mFirstKnownItem.lowerLimit
+ ^ mLastKnownItem.upperLimit
+ ^ limitsBeforeCoordinate.upperLimit
+ ^ limitsBeforeCoordinate.lowerLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativeCoordinate)) {
+ return false;
+ }
+
+ RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
+ return toComparisonValue() == otherCoordinate.toComparisonValue();
+ }
+
+ @Override
+ public int compareTo(RelativeCoordinate other) {
+ return toComparisonValue() - other.toComparisonValue();
+ }
+ }
+
+ RelativePoint createRelativePoint(Point point) {
+ return new RelativePoint(
+ new RelativeCoordinate(mColumnBounds, point.x),
+ new RelativeCoordinate(mRowBounds, point.y));
+ }
+
+ /**
+ * The location of a point relative to the Limits of nearby items; consists of both an x- and
+ * y-RelativeCoordinateLocation.
+ */
+ private static class RelativePoint {
+
+ final RelativeCoordinate mX;
+ final RelativeCoordinate mY;
+
+ RelativePoint(List<Limits> columnLimits, List<Limits> rowLimits, Point point) {
+ this.mX = new RelativeCoordinate(columnLimits, point.x);
+ this.mY = new RelativeCoordinate(rowLimits, point.y);
+ }
+
+ RelativePoint(RelativeCoordinate x, RelativeCoordinate y) {
+ this.mX = x;
+ this.mY = y;
+ }
+
+ @Override
+ public int hashCode() {
+ return mX.toComparisonValue() ^ mY.toComparisonValue();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativePoint)) {
+ return false;
+ }
+
+ RelativePoint otherPoint = (RelativePoint) other;
+ return mX.equals(otherPoint.mX) && mY.equals(otherPoint.mY);
+ }
+ }
+
+ /**
+ * Generates a rectangle which contains the items selected by the pointer and origin.
+ *
+ * @return The rectangle, or null if no items were selected.
+ */
+ private Rect computeBounds() {
+ Rect rect = new Rect();
+ rect.left = getCoordinateValue(
+ min(mRelOrigin.mX, mRelPointer.mX),
+ mColumnBounds,
+ true);
+ rect.right = getCoordinateValue(
+ max(mRelOrigin.mX, mRelPointer.mX),
+ mColumnBounds,
+ false);
+ rect.top = getCoordinateValue(
+ min(mRelOrigin.mY, mRelPointer.mY),
+ mRowBounds,
+ true);
+ rect.bottom = getCoordinateValue(
+ max(mRelOrigin.mY, mRelPointer.mY),
+ mRowBounds,
+ false);
+ return rect;
+ }
+
+ /**
+ * Computes the corner of the selection nearest the origin.
+ */
+ private int computeCornerNearestOrigin() {
+ int cornerValue = 0;
+
+ if (mRelOrigin.mY.equals(min(mRelOrigin.mY, mRelPointer.mY))) {
+ cornerValue |= UPPER;
+ } else {
+ cornerValue |= LOWER;
+ }
+
+ if (mRelOrigin.mX.equals(min(mRelOrigin.mX, mRelPointer.mX))) {
+ cornerValue |= LEFT;
+ } else {
+ cornerValue |= RIGHT;
+ }
+
+ return cornerValue;
+ }
+
+ private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
+ return first.compareTo(second) < 0 ? first : second;
+ }
+
+ private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
+ return first.compareTo(second) > 0 ? first : second;
+ }
+
+ /**
+ * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+ * coordinate.
+ */
+ private int getCoordinateValue(
+ RelativeCoordinate coordinate, List<Limits> limitsList, boolean isStartOfRange) {
+
+ switch (coordinate.type) {
+ case RelativeCoordinate.BEFORE_FIRST_ITEM:
+ return limitsList.get(0).lowerLimit;
+ case RelativeCoordinate.AFTER_LAST_ITEM:
+ return limitsList.get(limitsList.size() - 1).upperLimit;
+ case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+ if (isStartOfRange) {
+ return coordinate.limitsAfterCoordinate.lowerLimit;
+ } else {
+ return coordinate.limitsBeforeCoordinate.upperLimit;
+ }
+ case RelativeCoordinate.WITHIN_LIMITS:
+ return coordinate.limitsBeforeCoordinate.lowerLimit;
+ }
+
+ throw new RuntimeException("Invalid coordinate value.");
+ }
+
+ private boolean areItemsCoveredByBand(
+ RelativePoint first, RelativePoint second) {
+
+ return doesCoordinateLocationCoverItems(first.mX, second.mX)
+ && doesCoordinateLocationCoverItems(first.mY, second.mY);
+ }
+
+ private boolean doesCoordinateLocationCoverItems(
+ RelativeCoordinate pointerCoordinate, RelativeCoordinate originCoordinate) {
+
+ if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM
+ && originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM
+ && originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+ && originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+ && pointerCoordinate.limitsBeforeCoordinate.equals(
+ originCoordinate.limitsBeforeCoordinate)
+ && pointerCoordinate.limitsAfterCoordinate.equals(
+ originCoordinate.limitsAfterCoordinate)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Provides functionality for BandController. Exists primarily to tests that are
+ * fully isolated from RecyclerView.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+ abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> {
+
+ /**
+ * Remove the listener.
+ *
+ * @param listener
+ */
+ abstract void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+
+ /**
+ * @param relativePoint for which to create absolute point.
+ * @return absolute point.
+ */
+ abstract Point createAbsolutePoint(Point relativePoint);
+
+ /**
+ * @param index index of child.
+ * @return rectangle describing child at {@code index}.
+ */
+ abstract Rect getAbsoluteRectForChildViewAt(int index);
+
+ /**
+ * @param index index of child.
+ * @return child adapter position for the child at {@code index}
+ */
+ abstract int getAdapterPositionAt(int index);
+
+ /** @return column count. */
+ abstract int getColumnCount();
+
+ /** @return number of children visible in the view. */
+ abstract int getVisibleChildCount();
+
+ /**
+ * @return true if the item at adapter position is attached to a view.
+ */
+ abstract boolean hasView(int adapterPosition);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
new file mode 100644
index 0000000..da30c97
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+/**
+ * Provides event handlers w/ access to details about documents details
+ * view items Documents in the UI (RecyclerView).
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ItemDetailsLookup<K> {
+
+ /** @return true if there is an item under the finger/cursor. */
+ public boolean overItem(MotionEvent e) {
+ return getItemPosition(e) != RecyclerView.NO_POSITION;
+ }
+
+ /** @return true if there is an item w/ a stable ID under the finger/cursor. */
+ public boolean overItemWithSelectionKey(MotionEvent e) {
+ return overItem(e) && hasSelectionKey(getItemDetails(e));
+ }
+
+ /**
+ * @return true if the event is over an area that can be dragged via touch
+ * or via mouse. List items have a white area that is not draggable.
+ */
+ public boolean inItemDragRegion(MotionEvent e) {
+ return overItem(e) && getItemDetails(e).inDragRegion(e);
+ }
+
+ /**
+ * @return true if the event is in the "selection hot spot" region.
+ * The hot spot region instantly selects in touch mode, vs launches.
+ */
+ public boolean inItemSelectRegion(MotionEvent e) {
+ return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
+ }
+
+ /**
+ * @return the adapter position of the item under the finger/cursor.
+ */
+ public int getItemPosition(MotionEvent e) {
+ @Nullable ItemDetails<?> item = getItemDetails(e);
+ return item != null
+ ? item.getPosition()
+ : RecyclerView.NO_POSITION;
+ }
+
+ private static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
+ return item != null && item.getSelectionKey() != null;
+ }
+
+ private static boolean hasPosition(@Nullable ItemDetails<?> item) {
+ return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+ }
+
+ /**
+ * @return the DocumentDetails for the item under the event, or null.
+ */
+ public abstract @Nullable ItemDetails<K> getItemDetails(MotionEvent e);
+
+ /**
+ * Abstract class providing helper classes with access to information about
+ * RecyclerView item associated with a MotionEvent.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+ // TODO: Can this be merged with ViewHolder?
+ public abstract static class ItemDetails<K> {
+
+ /** @return the position of an item. */
+ public abstract int getPosition();
+
+ /** @return true if the item has a stable id. */
+ public boolean hasSelectionKey() {
+ return getSelectionKey() != null;
+ }
+
+ /** @return the stable id of an item. */
+ public abstract @Nullable K getSelectionKey();
+
+ /**
+ * @return true if the event is in an area of the item that should be
+ * directly interpreted as a user wishing to select the item. This
+ * is useful for checkboxes and other UI affordances focused on enabling
+ * selection.
+ */
+ public boolean inSelectionHotspot(MotionEvent e) {
+ return false;
+ }
+
+ /**
+ * Events in the drag region will dealt with differently that events outside
+ * of the drag region. This allows the client to implement custom handling
+ * for events related to drag and drop.
+ */
+ public boolean inDragRegion(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ItemDetails) {
+ return isEqualTo((ItemDetails) obj);
+ }
+ return false;
+ }
+
+ private boolean isEqualTo(ItemDetails other) {
+ K key = getSelectionKey();
+ boolean sameKeys = false;
+ if (key == null) {
+ sameKeys = other.getSelectionKey() == null;
+ } else {
+ sameKeys = key.equals(other.getSelectionKey());
+ }
+ return sameKeys && this.getPosition() == other.getPosition();
+ }
+
+ @Override
+ public int hashCode() {
+ return getPosition() >>> 8;
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
new file mode 100644
index 0000000..134c442
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides support for sting based stable ids in the RecyclerView selection helper.
+ * Client code can use this to look up stable ids when working with selection
+ * in application code.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ItemKeyProvider<K> {
+
+ /**
+ * Provides access to all data, regardless of whether it is bound to a view or not.
+ * Key providers with this access type enjoy support for enhanced features like:
+ * SHIFT+click range selection, and band selection.
+ */
+ @VisibleForTesting // otherwise protected would do nicely.
+ public static final int SCOPE_MAPPED = 0;
+
+ /**
+ * Provides access cached data based on what was recently bound in the view.
+ * Employing this provider will result in a reduced feature-set, as some
+ * featuers like SHIFT+click range selection and band selection are dependent
+ * on mapped access.
+ */
+ @VisibleForTesting // otherwise protected would do nicely.
+ public static final int SCOPE_CACHED = 1;
+
+ @IntDef({
+ SCOPE_MAPPED,
+ SCOPE_CACHED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ protected @interface Scope {}
+
+ private final @Scope int mScope;
+
+ /**
+ * Creates a new provider with the given scope.
+ * @param scope Scope can't change at runtime (at least code won't adapt)
+ * so it must be specified in the constructor.
+ */
+ protected ItemKeyProvider(@Scope int scope) {
+ checkArgument(scope == SCOPE_MAPPED || scope == SCOPE_CACHED);
+
+ mScope = scope;
+ }
+
+ final boolean hasAccess(@Scope int scope) {
+ return scope == mScope;
+ }
+
+ /**
+ * @return The selection key of the item at the given adapter position.
+ */
+ public abstract @Nullable K getKey(int position);
+
+ /**
+ * @return the position of a stable ID, or RecyclerView.NO_POSITION.
+ */
+ public abstract int getPosition(K key);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java
new file mode 100644
index 0000000..dd9e54f
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.graphics.Point;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Utility methods for working with {@link MotionEvent} instances.
+ */
+final class MotionEvents {
+
+ private MotionEvents() {}
+
+ static boolean isMouseEvent(MotionEvent e) {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
+ }
+
+ static boolean isTouchEvent(MotionEvent e) {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
+ }
+
+ static boolean isActionMove(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_MOVE;
+ }
+
+ static boolean isActionDown(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_DOWN;
+ }
+
+ static boolean isActionUp(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_UP;
+ }
+
+ static boolean isActionPointerUp(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
+ }
+
+ @SuppressWarnings("unused")
+ static boolean isActionPointerDown(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+ }
+
+ static boolean isActionCancel(MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
+ }
+
+ static Point getOrigin(MotionEvent e) {
+ return new Point((int) e.getX(), (int) e.getY());
+ }
+
+ static boolean isPrimaryButtonPressed(MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_PRIMARY);
+ }
+
+ static boolean isSecondaryButtonPressed(MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_SECONDARY);
+ }
+
+ static boolean isTertiaryButtonPressed(MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_TERTIARY);
+ }
+
+ // TODO: Replace with MotionEvent.isButtonPressed once targeting 21 or higher.
+ private static boolean isButtonPressed(MotionEvent e, int button) {
+ if (button == 0) {
+ return false;
+ }
+ return (e.getButtonState() & button) == button;
+ }
+
+ static boolean isShiftKeyPressed(MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
+ }
+
+ static boolean isCtrlKeyPressed(MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
+ }
+
+ static boolean isAltKeyPressed(MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
+ }
+
+ static boolean isTouchpadScroll(MotionEvent e) {
+ // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
+ // returned.
+ return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
+ }
+
+ private static boolean hasBit(int metaState, int bit) {
+ return (metaState & bit) != 0;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
new file mode 100644
index 0000000..1c06302
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Base class for handlers that can be registered w/ {@link GestureRouter}.
+ */
+abstract class MotionInputHandler<K> extends SimpleOnGestureListener {
+
+ protected final SelectionHelper<K> mSelectionHelper;
+
+ private final ItemKeyProvider<K> mKeyProvider;
+ private final FocusCallbacks<K> mFocusCallbacks;
+
+ MotionInputHandler(
+ SelectionHelper<K> selectionHelper,
+ ItemKeyProvider<K> keyProvider,
+ FocusCallbacks<K> focusCallbacks) {
+
+ checkArgument(selectionHelper != null);
+ checkArgument(keyProvider != null);
+ checkArgument(focusCallbacks != null);
+
+ mSelectionHelper = selectionHelper;
+ mKeyProvider = keyProvider;
+ mFocusCallbacks = focusCallbacks;
+ }
+
+ final boolean selectItem(ItemDetails<K> details) {
+ checkArgument(details != null);
+ checkArgument(hasPosition(details));
+ checkArgument(hasSelectionKey(details));
+
+ if (mSelectionHelper.select(details.getSelectionKey())) {
+ mSelectionHelper.anchorRange(details.getPosition());
+ }
+
+ // we set the focus on this doc so it will be the origin for keyboard events or shift+clicks
+ // if there is only a single item selected, otherwise clear focus
+ if (mSelectionHelper.getSelection().size() == 1) {
+ mFocusCallbacks.focusItem(details);
+ } else {
+ mFocusCallbacks.clearFocus();
+ }
+ return true;
+ }
+
+ protected final boolean focusItem(ItemDetails<K> details) {
+ checkArgument(details != null);
+ checkArgument(hasSelectionKey(details));
+
+ mSelectionHelper.clearSelection();
+ mFocusCallbacks.focusItem(details);
+ return true;
+ }
+
+ protected final void extendSelectionRange(ItemDetails<K> details) {
+ checkState(mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED));
+ checkArgument(hasPosition(details));
+ checkArgument(hasSelectionKey(details));
+
+ mSelectionHelper.extendRange(details.getPosition());
+ mFocusCallbacks.focusItem(details);
+ }
+
+ final boolean isRangeExtension(MotionEvent e) {
+ return MotionEvents.isShiftKeyPressed(e)
+ && mSelectionHelper.isRangeActive()
+ // Without full corpus access we can't reliably implement range
+ // as a user can scroll *anywhere* then SHIFT+click.
+ && mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED);
+ }
+
+ boolean shouldClearSelection(MotionEvent e, ItemDetails<K> item) {
+ return !MotionEvents.isCtrlKeyPressed(e)
+ && !item.inSelectionHotspot(e)
+ && !mSelectionHelper.isSelected(item.getSelectionKey());
+ }
+
+ static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
+ return item != null && item.getSelectionKey() != null;
+ }
+
+ static boolean hasPosition(@Nullable ItemDetails<?> item) {
+ return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java
new file mode 100644
index 0000000..05c47c1
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MouseCallbacks {
+
+ static final MouseCallbacks DUMMY = new MouseCallbacks() {
+ @Override
+ public boolean onContextClick(MotionEvent e) {
+ return false;
+ }
+ };
+
+ /**
+ * Called when user performs a context click, usually via mouse pointer
+ * right-click.
+ *
+ * @param e the event associated with the click.
+ * @return true if the event was handled.
+ */
+ public abstract boolean onContextClick(MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
new file mode 100644
index 0000000..0b4ea2c
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for mouse/stylus driven selection. This
+ * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
+ * to provide robust user drive selection support.
+ */
+final class MouseInputHandler<K> extends MotionInputHandler<K> {
+
+ private static final String TAG = "MouseInputDelegate";
+
+ private final ItemDetailsLookup<K> mDetailsLookup;
+ private final MouseCallbacks mMouseCallbacks;
+ private final ActivationCallbacks<K> mActivationCallbacks;
+ private final FocusCallbacks<K> mFocusCallbacks;
+
+ // The event has been handled in onSingleTapUp
+ private boolean mHandledTapUp;
+ // true when the previous event has consumed a right click motion event
+ private boolean mHandledOnDown;
+
+ MouseInputHandler(
+ SelectionHelper<K> selectionHelper,
+ ItemKeyProvider<K> keyProvider,
+ ItemDetailsLookup<K> detailsLookup,
+ MouseCallbacks mouseCallbacks,
+ ActivationCallbacks<K> activationCallbacks,
+ FocusCallbacks<K> focusCallbacks) {
+
+ super(selectionHelper, keyProvider, focusCallbacks);
+
+ checkArgument(detailsLookup != null);
+ checkArgument(mouseCallbacks != null);
+ checkArgument(activationCallbacks != null);
+
+ mDetailsLookup = detailsLookup;
+ mMouseCallbacks = mouseCallbacks;
+ mActivationCallbacks = activationCallbacks;
+ mFocusCallbacks = focusCallbacks;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
+ if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryButtonPressed(e))
+ || MotionEvents.isSecondaryButtonPressed(e)) {
+ mHandledOnDown = true;
+ return onRightClick(e);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ // Don't scroll content window in response to mouse drag
+ // If it's two-finger trackpad scrolling, we want to scroll
+ return !MotionEvents.isTouchpadScroll(e2);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // See b/27377794. Since we don't get a button state back from UP events, we have to
+ // explicitly save this state to know whether something was previously handled by
+ // DOWN events or not.
+ if (mHandledOnDown) {
+ if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown.");
+ mHandledOnDown = false;
+ return false;
+ }
+
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
+ mSelectionHelper.clearSelection();
+ mFocusCallbacks.clearFocus();
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ if (mSelectionHelper.hasSelection()) {
+ onItemClick(e, mDetailsLookup.getItemDetails(e));
+ mHandledTapUp = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ // tap on an item when there is an existing selection. We could extend
+ // a selection, we could clear selection (then launch)
+ private void onItemClick(MotionEvent e, ItemDetails<K> item) {
+ checkState(mSelectionHelper.hasSelection());
+ checkArgument(item != null);
+
+ if (isRangeExtension(e)) {
+ extendSelectionRange(item);
+ } else {
+ if (shouldClearSelection(e, item)) {
+ mSelectionHelper.clearSelection();
+ }
+ if (mSelectionHelper.isSelected(item.getSelectionKey())) {
+ if (mSelectionHelper.deselect(item.getSelectionKey())) {
+ mFocusCallbacks.clearFocus();
+ }
+ } else {
+ selectOrFocusItem(item, e);
+ }
+ }
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mHandledTapUp) {
+ if (VERBOSE) {
+ Log.v(TAG,
+ "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
+ }
+ mHandledTapUp = false;
+ return false;
+ }
+
+ if (mSelectionHelper.hasSelection()) {
+ return false; // should have been handled by onSingleTapUp.
+ }
+
+ if (!mDetailsLookup.overItem(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item.");
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+ if (item == null || !item.hasSelectionKey()) {
+ return false;
+ }
+
+ if (mFocusCallbacks.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
+ mSelectionHelper.startRange(mFocusCallbacks.getFocusedPosition());
+ mSelectionHelper.extendRange(item.getPosition());
+ } else {
+ selectOrFocusItem(item, e);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ mHandledTapUp = false;
+
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item.");
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+ return (item != null) && mActivationCallbacks.onItemActivated(item, e);
+ }
+
+ private boolean onRightClick(MotionEvent e) {
+ if (mDetailsLookup.overItemWithSelectionKey(e)) {
+ @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+ if (item != null && !mSelectionHelper.isSelected(item.getSelectionKey())) {
+ mSelectionHelper.clearSelection();
+ selectItem(item);
+ }
+ }
+
+ // We always delegate final handling of the event,
+ // since the handler might want to show a context menu
+ // in an empty area or some other weirdo view.
+ return mMouseCallbacks.onContextClick(e);
+ }
+
+ private void selectOrFocusItem(ItemDetails<K> item, MotionEvent e) {
+ if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
+ selectItem(item);
+ } else {
+ focusItem(item);
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java
new file mode 100644
index 0000000..6e11698
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * Subclass of Selection exposing public support for mutating the underlying selection data.
+ * This is useful for clients of {@link SelectionHelper} that wish to manipulate
+ * a copy of selection data obtained via {@link SelectionHelper#copySelection(Selection)}.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class MutableSelection<K> extends Selection<K> {
+
+ @Override
+ public boolean add(K key) {
+ return super.add(key);
+ }
+
+ @Override
+ public boolean remove(K key) {
+ return super.remove(key);
+ }
+
+ @Override
+ public void copyFrom(Selection<K> source) {
+ super.copyFrom(source);
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java
new file mode 100644
index 0000000..632e436
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.support.annotation.IntDef;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class providing support for managing range selections.
+ */
+final class Range {
+
+ static final int TYPE_PRIMARY = 0;
+
+ /**
+ * "Provisional" selection represents a overlay on the primary selection. A provisional
+ * selection maybe be eventually added to the primary selection, or it may be abandoned.
+ *
+ * <p>E.g. BandSelectionHelper creates a provisional selection while a user is actively
+ * selecting items with a band. GestureSelectionHelper creates a provisional selection
+ * while a user is active selecting via gesture.
+ *
+ * <p>Provisionally selected items are considered to be selected in
+ * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
+ * merged into the promary selection.
+ *
+ * <p>A provisional selection may intersect with the primary selection, however clearing the
+ * provisional selection will not affect the primary selection where the two may intersect.
+ */
+ static final int TYPE_PROVISIONAL = 1;
+ @IntDef({
+ TYPE_PRIMARY,
+ TYPE_PROVISIONAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RangeType {}
+
+ private static final String TAG = "Range";
+
+ private final Callbacks mCallbacks;
+ private final int mBegin;
+ private int mEnd = NO_POSITION;
+
+ /**
+ * Creates a new range anchored at {@code position}.
+ *
+ * @param position
+ * @param callbacks
+ */
+ Range(int position, Callbacks callbacks) {
+ mBegin = position;
+ mCallbacks = callbacks;
+ if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
+ }
+
+ void extendRange(int position, @RangeType int type) {
+ checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
+
+ if (mEnd == NO_POSITION || mEnd == mBegin) {
+ // Reset mEnd so it can be established in establishRange.
+ mEnd = NO_POSITION;
+ establishRange(position, type);
+ } else {
+ reviseRange(position, type);
+ }
+ }
+
+ private void establishRange(int position, @RangeType int type) {
+ checkArgument(mEnd == NO_POSITION, "End has already been set.");
+
+ mEnd = position;
+
+ if (position > mBegin) {
+ if (DEBUG) log(type, "Establishing initial range at @ " + position);
+ updateRange(mBegin + 1, position, true, type);
+ } else if (position < mBegin) {
+ if (DEBUG) log(type, "Establishing initial range at @ " + position);
+ updateRange(position, mBegin - 1, true, type);
+ }
+ }
+
+ private void reviseRange(int position, @RangeType int type) {
+ checkArgument(mEnd != NO_POSITION, "End must already be set.");
+ checkArgument(mBegin != mEnd, "Beging and end point to same position.");
+
+ if (position == mEnd) {
+ if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
+ }
+
+ if (mEnd > mBegin) {
+ reviseAscending(position, type);
+ } else if (mEnd < mBegin) {
+ reviseDescending(position, type);
+ }
+ // the "else" case is covered by checkState at beginning of method.
+
+ mEnd = position;
+ }
+
+ /**
+ * Updates an existing ascending selection.
+ */
+ private void reviseAscending(int position, @RangeType int type) {
+ if (DEBUG) log(type, "*ascending* Revising range @ " + position);
+
+ if (position < mEnd) {
+ if (position < mBegin) {
+ updateRange(mBegin + 1, mEnd, false, type);
+ updateRange(position, mBegin - 1, true, type);
+ } else {
+ updateRange(position + 1, mEnd, false, type);
+ }
+ } else if (position > mEnd) { // Extending the range...
+ updateRange(mEnd + 1, position, true, type);
+ }
+ }
+
+ private void reviseDescending(int position, @RangeType int type) {
+ if (DEBUG) log(type, "*descending* Revising range @ " + position);
+
+ if (position > mEnd) {
+ if (position > mBegin) {
+ updateRange(mEnd, mBegin - 1, false, type);
+ updateRange(mBegin + 1, position, true, type);
+ } else {
+ updateRange(mEnd, position - 1, false, type);
+ }
+ } else if (position < mEnd) { // Extending the range...
+ updateRange(position, mEnd - 1, true, type);
+ }
+ }
+
+ /**
+ * Try to set selection state for all elements in range. Not that callbacks can cancel
+ * selection of specific items, so some or even all items may not reflect the desired state
+ * after the update is complete.
+ *
+ * @param begin Adapter position for range start (inclusive).
+ * @param end Adapter position for range end (inclusive).
+ * @param selected New selection state.
+ */
+ private void updateRange(
+ int begin, int end, boolean selected, @RangeType int type) {
+ mCallbacks.updateForRange(begin, end, selected, type);
+ }
+
+ @Override
+ public String toString() {
+ return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
+ }
+
+ private void log(@RangeType int type, String message) {
+ String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
+ Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
+ }
+
+ /*
+ * @see {@link DefaultSelectionHelper#updateForRange(int, int , boolean, int)}.
+ */
+ abstract static class Callbacks {
+ abstract void updateForRange(
+ int begin, int end, boolean selected, @RangeType int type);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java
new file mode 100644
index 0000000..a622530
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Object representing the current selection and provisional selection. Provides read only public
+ * access, and private write access.
+ * <p>
+ * This class tracks selected items by managing two sets:
+ *
+ * <li>primary selection
+ *
+ * Primary selection consists of items tapped by a user or by lassoed by band select operation.
+ *
+ * <li>provisional selection
+ *
+ * Provisional selections are selections which have been temporarily created
+ * by an in-progress band select or gesture selection. Once the user releases the mouse button
+ * or lifts their finger the corresponding provisional selection should be converted into
+ * primary selection.
+ *
+ * <p>The total selection is the combination of
+ * both the core selection and the provisional selection. Tracking both separately is necessary to
+ * ensure that items in the core selection are not "erased" from the core selection when they
+ * are temporarily included in a secondary selection (like band selection).
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class Selection<K> implements Iterable<K> {
+
+ // NOTE: Not currently private as DefaultSelectionHelper directly manipulates values.
+ final Set<K> mSelection;
+ final Set<K> mProvisionalSelection;
+
+ Selection() {
+ mSelection = new HashSet<>();
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * Used by {@link SelectionStorage} when restoring selection.
+ */
+ Selection(Set<K> selection) {
+ mSelection = selection;
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * @param key
+ * @return true if the position is currently selected.
+ */
+ public boolean contains(@Nullable K key) {
+ return mSelection.contains(key) || mProvisionalSelection.contains(key);
+ }
+
+ /**
+ * Returns an {@link Iterator} that iterators over the selection, *excluding*
+ * any provisional selection.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator<K> iterator() {
+ return mSelection.iterator();
+ }
+
+ /**
+ * @return size of the selection including both final and provisional selected items.
+ */
+ public int size() {
+ return mSelection.size() + mProvisionalSelection.size();
+ }
+
+ /**
+ * @return true if the selection is empty.
+ */
+ public boolean isEmpty() {
+ return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
+ }
+
+ /**
+ * Sets the provisional selection, which is a temporary selection that can be saved,
+ * canceled, or adjusted at a later time. When a new provision selection is applied, the old
+ * one (if it exists) is abandoned.
+ * @return Map of ids added or removed. Added ids have a value of true, removed are false.
+ */
+ Map<K, Boolean> setProvisionalSelection(Set<K> newSelection) {
+ Map<K, Boolean> delta = new HashMap<>();
+
+ for (K key: mProvisionalSelection) {
+ // Mark each item that used to be in the provisional selection
+ // but is not in the new provisional selection.
+ if (!newSelection.contains(key) && !mSelection.contains(key)) {
+ delta.put(key, false);
+ }
+ }
+
+ for (K key: mSelection) {
+ // Mark each item that used to be in the selection but is unsaved and not in the new
+ // provisional selection.
+ if (!newSelection.contains(key)) {
+ delta.put(key, false);
+ }
+ }
+
+ for (K key: newSelection) {
+ // Mark each item that was not previously in the selection but is in the new
+ // provisional selection.
+ if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
+ delta.put(key, true);
+ }
+ }
+
+ // Now, iterate through the changes and actually add/remove them to/from the current
+ // selection. This could not be done in the previous loops because changing the size of
+ // the selection mid-iteration changes iteration order erroneously.
+ for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
+ K key = entry.getKey();
+ if (entry.getValue()) {
+ mProvisionalSelection.add(key);
+ } else {
+ mProvisionalSelection.remove(key);
+ }
+ }
+
+ return delta;
+ }
+
+ /**
+ * Saves the existing provisional selection. Once the provisional selection is saved,
+ * subsequent provisional selections which are different from this existing one cannot
+ * cause items in this existing provisional selection to become deselected.
+ */
+ @VisibleForTesting
+ protected void mergeProvisionalSelection() {
+ mSelection.addAll(mProvisionalSelection);
+ mProvisionalSelection.clear();
+ }
+
+ /**
+ * Abandons the existing provisional selection so that all items provisionally selected are
+ * now deselected.
+ */
+ @VisibleForTesting
+ void clearProvisionalSelection() {
+ mProvisionalSelection.clear();
+ }
+
+ /**
+ * Adds a new item to the primary selection.
+ *
+ * @return true if the operation resulted in a modification to the selection.
+ */
+ boolean add(K key) {
+ if (mSelection.contains(key)) {
+ return false;
+ }
+
+ mSelection.add(key);
+ return true;
+ }
+
+ /**
+ * Removes an item from the primary selection.
+ *
+ * @return true if the operation resulted in a modification to the selection.
+ */
+ boolean remove(K key) {
+ if (!mSelection.contains(key)) {
+ return false;
+ }
+
+ mSelection.remove(key);
+ return true;
+ }
+
+ /**
+ * Clears the primary selection. The provisional selection, if any, is unaffected.
+ */
+ void clear() {
+ mSelection.clear();
+ }
+
+ /**
+ * Clones primary and provisional selection from supplied {@link Selection}.
+ * Does not copy active range data.
+ */
+ void copyFrom(Selection<K> source) {
+ mSelection.clear();
+ mSelection.addAll(source.mSelection);
+
+ mProvisionalSelection.clear();
+ mProvisionalSelection.addAll(source.mProvisionalSelection);
+ }
+
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "size=0, items=[]";
+ }
+
+ StringBuilder buffer = new StringBuilder(size() * 28);
+ buffer.append("Selection{")
+ .append("primary{size=" + mSelection.size())
+ .append(", entries=" + mSelection)
+ .append("}, provisional{size=" + mProvisionalSelection.size())
+ .append(", entries=" + mProvisionalSelection)
+ .append("}}");
+ return buffer.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ return other instanceof Selection && isEqualTo((Selection) other);
+ }
+
+ private boolean isEqualTo(Selection other) {
+ return mSelection.equals(other.mSelection)
+ && mProvisionalSelection.equals(other.mProvisionalSelection);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java
new file mode 100644
index 0000000..276f903
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.Set;
+
+/**
+ * SelectionManager provides support for managing selection within a RecyclerView instance.
+ *
+ * @see DefaultSelectionHelper for details on instantiation.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SelectionHelper<K> {
+
+ /**
+ * This value is included in the payload when SelectionHelper implementations
+ * notify RecyclerView of changes. Clients can look for this in
+ * {@code onBindViewHolder} to know if the bind event is occurring in response
+ * to a selection state change.
+ */
+ public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
+
+ /**
+ * Adds {@code observer} to be notified when changes to selection occur.
+ * This method allows observers to closely track changes to selection
+ * avoiding the need to poll selection at performance critical points.
+ */
+ public abstract void addObserver(SelectionObserver observer);
+
+ /** @return true if has a selection */
+ public abstract boolean hasSelection();
+
+ /**
+ * Returns a Selection object that provides a live view on the current selection.
+ *
+ * @return The current selection.
+ * @see #copySelection(Selection) on how to get a snapshot
+ * of the selection that will not reflect future changes
+ * to selection.
+ */
+ public abstract Selection getSelection();
+
+ /**
+ * Updates {@code dest} to reflect the current selection.
+ */
+ public abstract void copySelection(Selection dest);
+
+ /**
+ * @return true if the item specified by its id is selected. Shorthand for
+ * {@code getSelection().contains(K)}.
+ */
+ public abstract boolean isSelected(@Nullable K key);
+
+ /**
+ * Restores the selected state of specified items. Used in cases such as restore the selection
+ * after rotation etc. Provisional selection, being provisional 'n all, isn't restored.
+ *
+ * <p>This affords clients the ability to restore selection from selection saved
+ * in Activity state. See {@link android.app.Activity#onCreate(Bundle)}.
+ *
+ * @param savedSelection selection being restored.
+ */
+ public abstract void restoreSelection(Selection savedSelection);
+
+ abstract void onDataSetChanged();
+
+ /**
+ * Clears both primary selection and provisional selection.
+ *
+ * @return true if anything changed.
+ */
+ public abstract boolean clear();
+
+ /**
+ * Clears the selection and notifies (if something changes).
+ */
+ public abstract void clearSelection();
+
+ /**
+ * Sets the selected state of the specified items. Note that the callback will NOT
+ * be consulted to see if an item can be selected.
+ */
+ public abstract boolean setItemsSelected(Iterable<K> keys, boolean selected);
+
+ /**
+ * Attempts to select an item.
+ *
+ * @return true if the item was selected. False if the item was not selected, or was
+ * was already selected prior to the method being called.
+ */
+ public abstract boolean select(K key);
+
+ /**
+ * Attempts to deselect an item.
+ *
+ * @return true if the item was deselected. False if the item was not deselected, or was
+ * was already deselected prior to the method being called.
+ */
+ public abstract boolean deselect(K key);
+
+ /**
+ * Selects the item at position and establishes the "anchor" for a range selection,
+ * replacing any existing range anchor.
+ *
+ * @param position The anchor position for the selection range.
+ */
+ public abstract void startRange(int position);
+
+ /**
+ * Sets the end point for the active range selection.
+ *
+ * <p>This function should only be called when a range selection is active
+ * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+ * selected.
+ *
+ * @param position The new end position for the selection range.
+ * @throws IllegalStateException if a range selection is not active. Range selection
+ * must have been started by a call to {@link #startRange(int)}.
+ */
+ public abstract void extendRange(int position);
+
+ /**
+ * Stops an in-progress range selection. All selection done with
+ * {@link #extendProvisionalRange(int)} will be lost if
+ * {@link Selection#mergeProvisionalSelection()} is not called beforehand.
+ */
+ public abstract void endRange();
+
+ /**
+ * @return Whether or not there is a current range selection active.
+ */
+ public abstract boolean isRangeActive();
+
+ /**
+ * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
+ * when determining how to extend, and modify selection ranges. Calling this when a
+ * range selection is active will reset the range selection.
+ *
+ * @param position the anchor position. Must already be selected.
+ */
+ protected abstract void anchorRange(int position);
+
+ /**
+ * @param position
+ */
+ // TODO: This is smelly. Maybe this type of logic needs to move into range selection,
+ // then selection manager can have a startProvisionalRange and startRange. Or
+ // maybe ranges always start life as provisional.
+ protected abstract void extendProvisionalRange(int position);
+
+ /**
+ * Sets the provisional selection, replacing any existing selection.
+ * @param newSelection
+ */
+ public abstract void setProvisionalSelection(Set<K> newSelection);
+
+ /** Clears any existing provisional selection */
+ public abstract void clearProvisionalSelection();
+
+ /**
+ * Converts the provisional selection into primary selection, then clears
+ * provisional selection.
+ */
+ public abstract void mergeProvisionalSelection();
+
+ /**
+ * Observer interface providing access to information about Selection state changes.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public abstract static class SelectionObserver<K> {
+
+ /**
+ * Called when state of an item has been changed.
+ */
+ public void onItemStateChanged(K key, boolean selected) {
+ }
+
+ /**
+ * Called when the underlying data set has change. After this method is called
+ * the selection manager will attempt traverse the existing selection,
+ * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
+ * and deselecting any items that cannot be selected given the updated dataset.
+ */
+ public void onSelectionReset() {
+ }
+
+ /**
+ * Called immediately after completion of any set of changes, excluding
+ * those resulting in calls to {@link #onSelectionReset()} and
+ * {@link #onSelectionRestored()}.
+ */
+ public void onSelectionChanged() {
+ }
+
+ /**
+ * Called immediately after selection is restored.
+ * {@link #onItemStateChanged(K, boolean)} will not be called
+ * for individual items in the selection.
+ */
+ public void onSelectionRestored() {
+ }
+ }
+
+ /**
+ * Implement SelectionPredicate to control when items can be selected or unselected.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public abstract static class SelectionPredicate<K> {
+
+ /** @return true if the item at {@code id} can be set to {@code nextState}. */
+ public abstract boolean canSetStateForKey(K key, boolean nextState);
+
+ /** @return true if the item at {@code id} can be set to {@code nextState}. */
+ public abstract boolean canSetStateAtPosition(int position, boolean nextState);
+
+ /** @return true if more than a single item can be selected. */
+ public abstract boolean canSelectMultiple();
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java
new file mode 100644
index 0000000..abdefaf
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Builder class for assembling selection support. Example usage:
+ *
+ * <p><pre>SelectionHelperBuilder selSupport = new SelectionHelperBuilder(
+ mRecView, new DemoStableIdProvider(mAdapter), detailsLookup);
+
+ // By default multi-select is supported.
+ SelectionHelper selHelper = selSupport
+ .build();
+
+ // This configuration support single selection for any element.
+ SelectionHelper selHelper = selSupport
+ .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+ .build();
+
+ // Lazily bind SelectionHelper. Allows us to defer initialization of the
+ // SelectionHelper dependency until after the adapter is created.
+ mAdapter.bindSelectionHelper(selHelper);
+
+ * </pre></p>
+ *
+ * @see SelectionStorage for important deatils on retaining selection across Activity
+ * lifecycle events.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionHelperBuilder<K> {
+
+ private final RecyclerView mRecView;
+ private final RecyclerView.Adapter<?> mAdapter;
+ private final Context mContext;
+
+ // Content lock provides a mechanism to block content reload while selection
+ // activities are active. If using a loader to load content, route
+ // the call through the content lock using ContentLock#runWhenUnlocked.
+ // This is especially useful when listening on content change notification.
+ private final ContentLock mLock = new ContentLock();
+
+ private SelectionPredicate<K> mSelectionPredicate = SelectionPredicates.selectAnything();
+ private ItemKeyProvider<K> mKeyProvider;
+ private ItemDetailsLookup<K> mDetailsLookup;
+
+ private ActivationCallbacks<K> mActivationCallbacks = ActivationCallbacks.dummy();
+ private FocusCallbacks<K> mFocusCallbacks = FocusCallbacks.dummy();
+ private TouchCallbacks mTouchCallbacks = TouchCallbacks.DUMMY;
+ private MouseCallbacks mMouseCallbacks = MouseCallbacks.DUMMY;
+
+ private BandPredicate mBandPredicate;
+ private int mBandOverlayId = R.drawable.selection_band_overlay;
+
+ private int[] mGestureToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_FINGER,
+ MotionEvent.TOOL_TYPE_UNKNOWN
+ };
+
+ private int[] mBandToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_MOUSE,
+ MotionEvent.TOOL_TYPE_STYLUS
+ };
+
+ public SelectionHelperBuilder(
+ RecyclerView recView,
+ ItemKeyProvider<K> keyProvider,
+ ItemDetailsLookup<K> detailsLookup) {
+
+ checkArgument(recView != null);
+
+ mRecView = recView;
+ mContext = recView.getContext();
+ mAdapter = recView.getAdapter();
+
+ checkArgument(mAdapter != null);
+ checkArgument(keyProvider != null);
+ checkArgument(detailsLookup != null);
+
+ mDetailsLookup = detailsLookup;
+ mKeyProvider = keyProvider;
+
+ mBandPredicate = BandPredicate.notDraggable(mRecView, detailsLookup);
+ }
+
+ /**
+ * Install seleciton predicate.
+ * @param predicate
+ * @return
+ */
+ public SelectionHelperBuilder<K> withSelectionPredicate(SelectionPredicate<K> predicate) {
+ checkArgument(predicate != null);
+ mSelectionPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * Add activation callbacks to respond to taps/enter/double-click on items.
+ *
+ * @param callbacks
+ * @return
+ */
+ public SelectionHelperBuilder<K> withActivationCallbacks(ActivationCallbacks<K> callbacks) {
+ checkArgument(callbacks != null);
+ mActivationCallbacks = callbacks;
+ return this;
+ }
+
+ /**
+ * Add focus callbacks to interfact with selection related focus changes.
+ * @param callbacks
+ * @return
+ */
+ public SelectionHelperBuilder<K> withFocusCallbacks(FocusCallbacks<K> callbacks) {
+ checkArgument(callbacks != null);
+ mFocusCallbacks = callbacks;
+ return this;
+ }
+
+ /**
+ * Configures mouse callbacks, replacing defaults.
+ *
+ * @param callbacks
+ * @return
+ */
+ public SelectionHelperBuilder<K> withMouseCallbacks(MouseCallbacks callbacks) {
+ checkArgument(callbacks != null);
+
+ mMouseCallbacks = callbacks;
+ return this;
+ }
+
+ /**
+ * Replaces default touch callbacks.
+ *
+ * @param callbacks
+ * @return
+ */
+ public SelectionHelperBuilder<K> withTouchCallbacks(TouchCallbacks callbacks) {
+ checkArgument(callbacks != null);
+
+ mTouchCallbacks = callbacks;
+ return this;
+ }
+
+ /**
+ * Replaces default gesture tooltypes.
+ * @param toolTypes
+ * @return
+ */
+ public SelectionHelperBuilder<K> withTouchTooltypes(int... toolTypes) {
+ mGestureToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Replaces default band overlay.
+ *
+ * @param bandOverlayId
+ * @return
+ */
+ public SelectionHelperBuilder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
+ mBandOverlayId = bandOverlayId;
+ return this;
+ }
+
+ /**
+ * Replaces default band predicate.
+ * @param bandPredicate
+ * @return
+ */
+ public SelectionHelperBuilder<K> withBandPredicate(BandPredicate bandPredicate) {
+
+ checkArgument(bandPredicate != null);
+
+ mBandPredicate = bandPredicate;
+ return this;
+ }
+
+ /**
+ * Replaces default band tools types.
+ * @param toolTypes
+ * @return
+ */
+ public SelectionHelperBuilder<K> withBandTooltypes(int... toolTypes) {
+ mBandToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Prepares selection support and returns the corresponding SelectionHelper.
+ *
+ * @return
+ */
+ public SelectionHelper<K> build() {
+
+ SelectionHelper<K> selectionHelper =
+ new DefaultSelectionHelper<>(mKeyProvider, mSelectionPredicate);
+
+ // Event glue between RecyclerView and SelectionHelper keeps the classes separate
+ // so that a SelectionHelper can be shared across RecyclerView instances that
+ // represent the same data in different ways.
+ EventBridge.install(mAdapter, selectionHelper, mKeyProvider);
+
+ AutoScroller scroller = new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecView));
+
+ // Setup basic input handling, with the touch handler as the default consumer
+ // of events. If mouse handling is configured as well, the mouse input
+ // related handlers will intercept mouse input events.
+
+ // GestureRouter is responsible for routing GestureDetector events
+ // to tool-type specific handlers.
+ GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
+ GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
+
+ // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
+ // Despite "Touch" being in the name, it receives events for all types of tools.
+ // This class is responsible for routing events to tool-type specific handlers,
+ // and if not handled by a handler, on to a GestureDetector for analysis.
+ TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
+
+ // GestureSelectionHelper provides logic that interprets a combination
+ // of motions and gestures in order to provide gesture driven selection support
+ // when used in conjunction with RecyclerView.
+ final GestureSelectionHelper gestureHelper =
+ GestureSelectionHelper.create(selectionHelper, mRecView, scroller, mLock);
+
+ // Finally hook the framework up to listening to recycle view events.
+ mRecView.addOnItemTouchListener(eventRouter);
+
+ // But before you move on, there's more work to do. Event plumbing has been
+ // installed, but we haven't registered any of our helpers or callbacks.
+ // Helpers contain predefined logic converting events into selection related events.
+ // Callbacks provide authors the ability to reponspond to other types of
+ // events (like "active" a tapped item). This is broken up into two main
+ // suites, one for "touch" and one for "mouse", though both can and should (usually)
+ // be configued to handle other types of input (to satisfy user expectation).);
+
+ // Provides high level glue for binding touch events
+ // and gestures to selection framework.
+ TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
+ selectionHelper,
+ mKeyProvider,
+ mDetailsLookup,
+ mSelectionPredicate,
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectionPredicate.canSelectMultiple()) {
+ gestureHelper.start();
+ }
+ }
+ },
+ mTouchCallbacks,
+ mActivationCallbacks,
+ mFocusCallbacks,
+ new Runnable() {
+ @Override
+ public void run() {
+ mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ });
+
+ for (int toolType : mGestureToolTypes) {
+ gestureRouter.register(toolType, touchHandler);
+ eventRouter.register(toolType, gestureHelper);
+ }
+
+ // Provides high level glue for binding mouse/stylus events and gestures
+ // to selection framework.
+ MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
+ selectionHelper,
+ mKeyProvider,
+ mDetailsLookup,
+ mMouseCallbacks,
+ mActivationCallbacks,
+ mFocusCallbacks);
+
+ for (int toolType : mBandToolTypes) {
+ gestureRouter.register(toolType, mouseHandler);
+ }
+
+ // Band selection not supported in single select mode, or when key access
+ // is limited to anything less than the entire corpus.
+ // TODO: Since we cach grid info from laid out items, we could cache key too.
+ // Then we couldn't have to limit to CORPUS access.
+ if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
+ && mSelectionPredicate.canSelectMultiple()) {
+ // BandSelectionHelper provides support for band selection on-top of a RecyclerView
+ // instance. Given the recycling nature of RecyclerView BandSelectionController
+ // necessarily models and caches list/grid information as the user's pointer
+ // interacts with the item in the RecyclerView. Selectable items that intersect
+ // with the band, both on and off screen, are selected.
+ BandSelectionHelper bandHelper = BandSelectionHelper.create(
+ mRecView,
+ scroller,
+ mBandOverlayId,
+ mKeyProvider,
+ selectionHelper,
+ mSelectionPredicate,
+ mBandPredicate,
+ mFocusCallbacks,
+ mLock);
+
+ for (int toolType : mBandToolTypes) {
+ eventRouter.register(toolType, bandHelper);
+ }
+ }
+
+ return selectionHelper;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
new file mode 100644
index 0000000..26253d9
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Utility class for creating SelectionPredicate instances.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionPredicates {
+
+ private SelectionPredicates() {}
+
+ /**
+ * Returns a selection predicate that allows multiples items to be selected, without
+ * any restrictions on which items can be selected.
+ * @param <K>
+ * @return
+ */
+ public static <K> SelectionPredicate<K> selectAnything() {
+ return new SelectionPredicate<K>() {
+ @Override
+ public boolean canSetStateForKey(K key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Returns a selection predicate that allows a single item to be selected, without
+ * any restrictions on which item can be selected.
+ * @param <K>
+ * @return
+ */
+ public static <K> SelectionPredicate<K> selectSingleAnything() {
+ return new SelectionPredicate<K>() {
+ @Override
+ public boolean canSetStateForKey(K key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return false;
+ }
+ };
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java
new file mode 100644
index 0000000..81db30f
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Helper class binding SelectionHelper and Activity lifecycle events facilitating
+ * persistence of selection across activity lifecycle events.
+ *
+ * <p>Usage:<br><pre>
+ void onCreate() {
+ mLifecycleHelper = new SelectionStorage<>(SelectionStorage.TYPE_STRING, mSelectionHelper);
+ if (savedInstanceState != null) {
+ mSelectionStorage.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mSelectionStorage.onSaveInstanceState(outState);
+ }
+ </pre>
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionStorage<K> {
+
+ @VisibleForTesting
+ static final String EXTRA_SAVED_SELECTION_TYPE = "androidx.recyclerview.selection.type";
+
+ @VisibleForTesting
+ static final String EXTRA_SAVED_SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
+
+ public static final int TYPE_STRING = 0;
+ public static final int TYPE_LONG = 1;
+ @IntDef({
+ TYPE_STRING,
+ TYPE_LONG
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface KeyType {}
+
+ private final @KeyType int mKeyType;
+ private final SelectionHelper<K> mHelper;
+
+ /**
+ * Creates a new lifecycle helper. {@code keyType}.
+ *
+ * @param keyType
+ * @param helper
+ */
+ public SelectionStorage(@KeyType int keyType, SelectionHelper<K> helper) {
+ checkArgument(
+ keyType == TYPE_STRING || keyType == TYPE_LONG,
+ "Only String and Integer presistence are supported by default.");
+ checkArgument(helper != null);
+
+ mKeyType = keyType;
+ mHelper = helper;
+ }
+
+ /**
+ * Preserves selection, if any.
+ *
+ * @param state
+ */
+ @SuppressWarnings("unchecked")
+ public void onSaveInstanceState(Bundle state) {
+ MutableSelection<K> sel = new MutableSelection<>();
+ mHelper.copySelection(sel);
+
+ state.putInt(EXTRA_SAVED_SELECTION_TYPE, mKeyType);
+ switch (mKeyType) {
+ case TYPE_STRING:
+ writeStringSelection(state, ((Selection<String>) sel).mSelection);
+ break;
+ case TYPE_LONG:
+ writeLongSelection(state, ((Selection<Long>) sel).mSelection);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported key type: " + mKeyType);
+ }
+ }
+
+ /**
+ * Restores selection from previously saved state.
+ *
+ * @param state
+ */
+ public void onRestoreInstanceState(@Nullable Bundle state) {
+ if (state == null) {
+ return;
+ }
+
+ int keyType = state.getInt(EXTRA_SAVED_SELECTION_TYPE, -1);
+ switch(keyType) {
+ case TYPE_STRING:
+ Selection<String> stringSel = readStringSelection(state);
+ if (stringSel != null && !stringSel.isEmpty()) {
+ mHelper.restoreSelection(stringSel);
+ }
+ break;
+ case TYPE_LONG:
+ Selection<Long> longSel = readLongSelection(state);
+ if (longSel != null && !longSel.isEmpty()) {
+ mHelper.restoreSelection(longSel);
+ }
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported selection key type.");
+ }
+ }
+
+ private @Nullable Selection<String> readStringSelection(@Nullable Bundle state) {
+ @Nullable ArrayList<String> stored =
+ state.getStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection<String> selection = new Selection<>();
+ selection.mSelection.addAll(stored);
+ return selection;
+ }
+
+ private @Nullable Selection<Long> readLongSelection(@Nullable Bundle state) {
+ @Nullable long[] stored = state.getLongArray(EXTRA_SAVED_SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection<Long> selection = new Selection<>();
+ for (long key : stored) {
+ selection.mSelection.add(key);
+ }
+ return selection;
+ }
+
+ private void writeStringSelection(Bundle state, Set<String> selected) {
+ ArrayList<String> value = new ArrayList<>(selected.size());
+ value.addAll(selected);
+ state.putStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES, value);
+ }
+
+ private void writeLongSelection(Bundle state, Set<Long> selected) {
+ long[] value = new long[selected.size()];
+ int i = 0;
+ for (Long key : selected) {
+ value[i++] = key;
+ }
+ state.putLongArray(EXTRA_SAVED_SELECTION_ENTRIES, value);
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java
new file mode 100644
index 0000000..3b79120
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+/**
+ * Shared constants used in this package.
+ */
+final class Shared {
+
+ static final boolean DEBUG = false;
+ static final boolean VERBOSE = true;
+
+ private Shared() {}
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
new file mode 100644
index 0000000..3dc78ca
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnChildAttachStateChangeListener;
+import android.util.SparseArray;
+import android.view.View;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ItemKeyProvider that provides stable ids by way of cached RecyclerView.Adapter stable ids.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
+
+ private final SparseArray<Long> mPositionToKey = new SparseArray<>();
+ private final Map<Long, Integer> mKeyToPosition = new HashMap<Long, Integer>();
+ private final RecyclerView mRecView;
+
+ public StableIdKeyProvider(RecyclerView recView) {
+
+ // Since this provide is based on stable ids based on whats laid out in the window
+ // we can only satisfy "window" scope key access.
+ super(SCOPE_CACHED);
+
+ mRecView = recView;
+
+ mRecView.addOnChildAttachStateChangeListener(
+ new OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(View view) {
+ onAttached(view);
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(View view) {
+ onDetached(view);
+ }
+ }
+ );
+
+ }
+
+ private void onAttached(View view) {
+ RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+ int position = holder.getAdapterPosition();
+ long id = holder.getItemId();
+ if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+ mPositionToKey.put(position, id);
+ mKeyToPosition.put(id, position);
+ }
+ }
+
+ private void onDetached(View view) {
+ RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+ int position = holder.getAdapterPosition();
+ long id = holder.getItemId();
+ if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+ mPositionToKey.delete(position);
+ mKeyToPosition.remove(id);
+ }
+ }
+
+ @Override
+ public @Nullable Long getKey(int position) {
+ return mPositionToKey.get(position, null);
+ }
+
+ @Override
+ public int getPosition(Long key) {
+ if (mKeyToPosition.containsKey(key)) {
+ return mKeyToPosition.get(key);
+ }
+ return RecyclerView.NO_POSITION;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
new file mode 100644
index 0000000..c735529
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Registry for tool specific event handler.
+ */
+final class ToolHandlerRegistry<T> {
+
+ // Currently there are four known input types. ERASER is the last one, so has the
+ // highest value. UNKNOWN is zero, so we add one. This allows delegates to be
+ // registered by type, and avoid the auto-boxing that would be necessary were we
+ // to store delegates in a Map<Integer, Delegate>.
+ private static final int sNumInputTypes = MotionEvent.TOOL_TYPE_ERASER + 1;
+
+ private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
+ private final T mDefault;
+
+ ToolHandlerRegistry(T defaultDelegate) {
+ checkArgument(defaultDelegate != null);
+ mDefault = defaultDelegate;
+
+ // Initialize all values to null.
+ for (int i = 0; i < sNumInputTypes; i++) {
+ mHandlers.set(i, null);
+ }
+ }
+
+ /**
+ * @param toolType
+ * @param delegate the delegate, or null to unregister.
+ * @throws IllegalStateException if an tooltype handler is already registered.
+ */
+ void set(int toolType, @Nullable T delegate) {
+ checkArgument(toolType >= 0 && toolType <= MotionEvent.TOOL_TYPE_ERASER);
+ checkState(mHandlers.get(toolType) == null);
+
+ mHandlers.set(toolType, delegate);
+ }
+
+ T get(MotionEvent e) {
+ T d = mHandlers.get(e.getToolType(0));
+ return d != null ? d : mDefault;
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java
new file mode 100644
index 0000000..5905392
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class TouchCallbacks {
+
+ static final TouchCallbacks DUMMY = new TouchCallbacks() {
+ @Override
+ public boolean onDragInitiated(MotionEvent e) {
+ return false;
+ }
+ };
+
+ /**
+ * Called when a drag is initiated. Touch input handler only considers
+ * a drag to be initiated on long press on an existing selection,
+ * as normal touch and drag events are strongly associated with scrolling of the view.
+ *
+ * <p>Drag will only be initiated when the item under the event is already selected.
+ *
+ * <p>The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
+ * to this method as there may be multiple items selected. Clients can obtain the current
+ * list of selected items from {@link SelectionHelper#copySelection(Selection)}.
+ *
+ * @param e the event associated with the drag.
+ * @return true if the event was handled.
+ */
+ public abstract boolean onDragInitiated(MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
new file mode 100644
index 0000000..fbbca23
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/**
+ * A class responsible for routing MotionEvents to tool-type specific handlers,
+ * and if not handled by a handler, on to a {@link GestureDetector} for further
+ * processing.
+ *
+ * <p>TouchEventRouter takes its name from
+ * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
+ * being in the name, it receives MotionEvents for all types of tools.
+ */
+final class TouchEventRouter implements OnItemTouchListener {
+
+ private static final String TAG = "TouchEventRouter";
+
+ private final GestureDetector mDetector;
+ private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
+
+ TouchEventRouter(GestureDetector detector, OnItemTouchListener defaultDelegate) {
+ checkArgument(detector != null);
+ checkArgument(defaultDelegate != null);
+
+ mDetector = detector;
+ mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+ }
+
+ TouchEventRouter(GestureDetector detector) {
+ this(
+ detector,
+ // Supply a fallback listener does nothing...because the caller
+ // didn't supply a fallback.
+ new OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+ });
+ }
+
+ /**
+ * @param toolType See MotionEvent for details on available types.
+ * @param delegate An {@link OnItemTouchListener} to receive events
+ * of {@code toolType}.
+ */
+ void register(int toolType, OnItemTouchListener delegate) {
+ checkArgument(delegate != null);
+ mDelegates.set(toolType, delegate);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
+
+ // Forward all events to UserInputHandler.
+ // This is necessary since UserInputHandler needs to always see the first DOWN event. Or
+ // else all future UP events will be tossed.
+ handled |= mDetector.onTouchEvent(e);
+
+ return handled;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ mDelegates.get(e).onTouchEvent(rv, e);
+
+ // Note: even though this event is being handled as part of gestures such as drag and band,
+ // continue forwarding to the GestureDetector. The detector needs to see the entire cluster
+ // of events in order to properly interpret other gestures, such as long press.
+ mDetector.onTouchEvent(e);
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
new file mode 100644
index 0000000..e07aeb1
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for touch driven selection. This class
+ * works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
+ * provide robust user drive selection support.
+ */
+final class TouchInputHandler<K> extends MotionInputHandler<K> {
+
+ private static final String TAG = "TouchInputDelegate";
+ private static final boolean DEBUG = false;
+
+ private final ItemDetailsLookup<K> mDetailsLookup;
+ private final SelectionPredicate<K> mSelectionPredicate;
+ private final ActivationCallbacks<K> mActivationCallbacks;
+ private final TouchCallbacks mTouchCallbacks;
+ private final Runnable mGestureStarter;
+ private final Runnable mHapticPerformer;
+
+ TouchInputHandler(
+ SelectionHelper<K> selectionHelper,
+ ItemKeyProvider<K> keyProvider,
+ ItemDetailsLookup<K> detailsLookup,
+ SelectionPredicate<K> selectionPredicate,
+ Runnable gestureStarter,
+ TouchCallbacks touchCallbacks,
+ ActivationCallbacks<K> activationCallbacks,
+ FocusCallbacks<K> focusCallbacks,
+ Runnable hapticPerformer) {
+
+ super(selectionHelper, keyProvider, focusCallbacks);
+
+ checkArgument(detailsLookup != null);
+ checkArgument(selectionPredicate != null);
+ checkArgument(gestureStarter != null);
+ checkArgument(activationCallbacks != null);
+ checkArgument(touchCallbacks != null);
+ checkArgument(hapticPerformer != null);
+
+ mDetailsLookup = detailsLookup;
+ mSelectionPredicate = selectionPredicate;
+ mGestureStarter = gestureStarter;
+ mActivationCallbacks = activationCallbacks;
+ mTouchCallbacks = touchCallbacks;
+ mHapticPerformer = hapticPerformer;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
+ mSelectionHelper.clearSelection();
+ return false;
+ }
+
+ ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+ // Should really not be null at this point, but...
+ if (item == null) {
+ return false;
+ }
+
+ if (mSelectionHelper.hasSelection()) {
+ if (isRangeExtension(e)) {
+ extendSelectionRange(item);
+ } else if (mSelectionHelper.isSelected(item.getSelectionKey())) {
+ mSelectionHelper.deselect(item.getSelectionKey());
+ } else {
+ selectItem(item);
+ }
+
+ return true;
+ }
+
+ // Touch events select if they occur in the selection hotspot,
+ // otherwise they activate.
+ return item.inSelectionHotspot(e)
+ ? selectItem(item)
+ : mActivationCallbacks.onItemActivated(item, e);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
+ return;
+ }
+
+ ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+ // Should really not be null at this point, but...
+ if (item == null) {
+ return;
+ }
+
+ boolean handled = false;
+
+ if (isRangeExtension(e)) {
+ extendSelectionRange(item);
+ handled = true;
+ } else {
+ if (!mSelectionHelper.isSelected(item.getSelectionKey())
+ && mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)) {
+ // If we cannot select it, we didn't apply anchoring - therefore should not
+ // start gesture selection
+ if (selectItem(item)) {
+ // And finally if the item was selected && we can select multiple
+ // we kick off gesture selection.
+ if (mSelectionPredicate.canSelectMultiple()) {
+ mGestureStarter.run();
+ }
+ handled = true;
+ }
+ } else {
+ // We only initiate drag and drop on long press for touch to allow regular
+ // touch-based scrolling
+ mTouchCallbacks.onDragInitiated(e);
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ mHapticPerformer.run();
+ }
+ }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
new file mode 100644
index 0000000..d13b0f2
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+/**
+ * Provides auto-scrolling upon request when user's interaction with the application
+ * introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,
+ * to provide auto scrolling when user is performing selection operations.
+ */
+final class ViewAutoScroller extends AutoScroller {
+
+ private static final String TAG = "ViewAutoScroller";
+
+ // ratio used to calculate the top/bottom hotspot region; used with view height
+ private static final float DEFAULT_SCROLL_THRESHOLD_RATIO = 0.125f;
+ private static final int MAX_SCROLL_STEP = 70;
+
+ private final float mScrollThresholdRatio;
+
+ private final ScrollHost mHost;
+ private final Runnable mRunner;
+
+ private @Nullable Point mOrigin;
+ private @Nullable Point mLastLocation;
+ private boolean mPassedInitialMotionThreshold;
+
+ ViewAutoScroller(ScrollHost scrollHost) {
+ this(scrollHost, DEFAULT_SCROLL_THRESHOLD_RATIO);
+ }
+
+ @VisibleForTesting
+ ViewAutoScroller(ScrollHost scrollHost, float scrollThresholdRatio) {
+
+ checkArgument(scrollHost != null);
+
+ mHost = scrollHost;
+ mScrollThresholdRatio = scrollThresholdRatio;
+
+ mRunner = new Runnable() {
+ @Override
+ public void run() {
+ runScroll();
+ }
+ };
+ }
+
+ @Override
+ protected void reset() {
+ mHost.removeCallback(mRunner);
+ mOrigin = null;
+ mLastLocation = null;
+ mPassedInitialMotionThreshold = false;
+ }
+
+ @Override
+ protected void scroll(Point location) {
+ mLastLocation = location;
+
+ // See #aboveMotionThreshold for details on how we track initial location.
+ if (mOrigin == null) {
+ mOrigin = location;
+ if (VERBOSE) Log.v(TAG, "Origin @ " + mOrigin);
+ }
+
+ if (VERBOSE) Log.v(TAG, "Current location @ " + mLastLocation);
+
+ mHost.runAtNextFrame(mRunner);
+ }
+
+ /**
+ * Attempts to smooth-scroll the view at the given UI frame. Application should be
+ * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
+ * finished, and re-run this method on the next UI frame if applicable.
+ */
+ private void runScroll() {
+ if (DEBUG) checkState(mLastLocation != null);
+
+ if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
+
+ // Compute the number of pixels the pointer's y-coordinate is past the view.
+ // Negative values mean the pointer is at or before the top of the view, and
+ // positive values mean that the pointer is at or after the bottom of the view. Note
+ // that top/bottom threshold is added here so that the view still scrolls when the
+ // pointer are in these buffer pixels.
+ int pixelsPastView = 0;
+
+ final int verticalThreshold = (int) (mHost.getViewHeight()
+ * mScrollThresholdRatio);
+
+ if (mLastLocation.y <= verticalThreshold) {
+ pixelsPastView = mLastLocation.y - verticalThreshold;
+ } else if (mLastLocation.y >= mHost.getViewHeight()
+ - verticalThreshold) {
+ pixelsPastView = mLastLocation.y - mHost.getViewHeight()
+ + verticalThreshold;
+ }
+
+ if (pixelsPastView == 0) {
+ // If the operation that started the scrolling is no longer inactive, or if it is active
+ // but not at the edge of the view, no scrolling is necessary.
+ return;
+ }
+
+ // We're in one of the endzones. Now determine if there's enough of a difference
+ // from the orgin to take any action. Basically if a user has somehow initiated
+ // selection, but is hovering at or near their initial contact point, we don't
+ // scroll. This avoids a situation where the user initiates selection in an "endzone"
+ // only to have scrolling start automatically.
+ if (!mPassedInitialMotionThreshold && !aboveMotionThreshold(mLastLocation)) {
+ if (VERBOSE) Log.v(TAG, "Ignoring event below motion threshold.");
+ return;
+ }
+ mPassedInitialMotionThreshold = true;
+
+ if (pixelsPastView > verticalThreshold) {
+ pixelsPastView = verticalThreshold;
+ }
+
+ // Compute the number of pixels to scroll, and scroll that many pixels.
+ final int numPixels = computeScrollDistance(pixelsPastView);
+ mHost.scrollBy(numPixels);
+
+ // Replace any existing scheduled jobs with the latest and greatest..
+ mHost.removeCallback(mRunner);
+ mHost.runAtNextFrame(mRunner);
+ }
+
+ private boolean aboveMotionThreshold(Point location) {
+ // We reuse the scroll threshold to calculate a much smaller area
+ // in which we ignore motion initially.
+ int motionThreshold =
+ (int) ((mHost.getViewHeight() * mScrollThresholdRatio)
+ * (mScrollThresholdRatio * 2));
+ return Math.abs(mOrigin.y - location.y) >= motionThreshold;
+ }
+
+ /**
+ * Computes the number of pixels to scroll based on how far the pointer is past the end
+ * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
+ * pixels to scroll when an item is dragged to the end of a view.
+ * @return
+ */
+ @VisibleForTesting
+ int computeScrollDistance(int pixelsPastView) {
+ final int topBottomThreshold =
+ (int) (mHost.getViewHeight() * mScrollThresholdRatio);
+
+ final int direction = (int) Math.signum(pixelsPastView);
+ final int absPastView = Math.abs(pixelsPastView);
+
+ // Calculate the ratio of how far out of the view the pointer currently resides to
+ // the top/bottom scrolling hotspot of the view.
+ final float outOfBoundsRatio = Math.min(
+ 1.0f, (float) absPastView / topBottomThreshold);
+ // Interpolate this ratio and use it to compute the maximum scroll that should be
+ // possible for this step.
+ final int cappedScrollStep =
+ (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
+
+ // If the final number of pixels to scroll ends up being 0, the view should still
+ // scroll at least one pixel.
+ return cappedScrollStep != 0 ? cappedScrollStep : direction;
+ }
+
+ /**
+ * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
+ * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
+ * drags that are at the edge or barely past the edge of the threshold does little to no
+ * scrolling, while drags that are near the edge of the view does a lot of
+ * scrolling. The equation y=x^10 is used, but this could also be tweaked if
+ * needed.
+ * @param ratio A ratio which is in the range [0, 1].
+ * @return A "smoothed" value, also in the range [0, 1].
+ */
+ private float smoothOutOfBoundsRatio(float ratio) {
+ return (float) Math.pow(ratio, 10);
+ }
+
+ /**
+ * Used by to calculate the proper amount of pixels to scroll given time passed
+ * since scroll started, and to properly scroll / proper listener clean up if necessary.
+ *
+ * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
+ * cycle.
+ */
+ abstract static class ScrollHost {
+ /**
+ * @return height of the view.
+ */
+ abstract int getViewHeight();
+
+ /**
+ * @param dy distance to scroll.
+ */
+ abstract void scrollBy(int dy);
+
+ /**
+ * @param r schedule runnable to be run at next convenient time.
+ */
+ abstract void runAtNextFrame(Runnable r);
+
+ /**
+ * @param r remove runnable from being run.
+ */
+ abstract void removeCallback(Runnable r);
+ }
+
+ public static ScrollHost createScrollHost(final RecyclerView view) {
+ return new RuntimeHost(view);
+ }
+
+ /**
+ * Tracks location of last surface contact as reported by RecyclerView.
+ */
+ private static final class RuntimeHost extends ScrollHost {
+
+ private final RecyclerView mRecView;
+
+ RuntimeHost(RecyclerView recView) {
+ mRecView = recView;
+ }
+
+ @Override
+ void runAtNextFrame(Runnable r) {
+ ViewCompat.postOnAnimation(mRecView, r);
+ }
+
+ @Override
+ void removeCallback(Runnable r) {
+ mRecView.removeCallbacks(r);
+ }
+
+ @Override
+ void scrollBy(int dy) {
+ if (VERBOSE) Log.v(TAG, "Scrolling view by: " + dy);
+ mRecView.scrollBy(0, dy);
+ }
+
+ @Override
+ int getViewHeight() {
+ return mRecView.getHeight();
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/AndroidManifest.xml b/recyclerview-selection/tests/AndroidManifest.xml
new file mode 100644
index 0000000..85f42d6
--- /dev/null
+++ b/recyclerview-selection/tests/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.recyclerview.selection.test">
+ <uses-sdk android:minSdkVersion="14" />
+
+ <application android:supportsRtl="true">
+ </application>
+</manifest>
diff --git a/recyclerview-selection/tests/NO_DOCS b/recyclerview-selection/tests/NO_DOCS
new file mode 100644
index 0000000..61c9b1a
--- /dev/null
+++ b/recyclerview-selection/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
new file mode 100644
index 0000000..8399539
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestAutoScroller;
+import androidx.recyclerview.selection.testing.TestBandPredicate;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents.Builder;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BandSelectionHelperTest {
+
+ private List<String> mItems;
+ private BandSelectionHelper<String> mBandController;
+ private boolean mIsActive;
+ private Builder mStartBuilder;
+ private Builder mStopBuilder;
+ private MotionEvent mStartEvent;
+ private MotionEvent mStopEvent;
+ private TestBandHost mBandHost;
+ private TestBandPredicate mBandPredicate;
+ private TestAdapter<String> mAdapter;
+
+ @Before
+ public void setup() throws Exception {
+ mItems = TestData.createStringData(10);
+ mIsActive = false;
+ mAdapter = new TestAdapter<String>();
+ mAdapter.updateTestModelIds(mItems);
+ mBandHost = new TestBandHost();
+ mBandPredicate = new TestBandPredicate();
+ ItemKeyProvider<String> keyProvider =
+ new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, mAdapter);
+ ContentLock contentLock = new ContentLock();
+ SelectionPredicate<String> selectionTest = SelectionPredicates.selectAnything();
+
+ SelectionHelper<String> helper = new DefaultSelectionHelper<String>(
+ keyProvider,
+ selectionTest);
+
+ EventBridge.install(mAdapter, helper, keyProvider);
+ FocusCallbacks<String> focusCallbacks = FocusCallbacks.dummy();
+
+ mBandController = new BandSelectionHelper<String>(
+ mBandHost,
+ new TestAutoScroller(),
+ keyProvider,
+ helper,
+ selectionTest,
+ mBandPredicate,
+ focusCallbacks,
+ contentLock) {
+ @Override
+ public boolean isActive() {
+ return mIsActive;
+ }
+ };
+
+ mStartBuilder = new Builder().mouse().primary().action(MotionEvent.ACTION_MOVE);
+ mStopBuilder = new Builder().mouse().action(MotionEvent.ACTION_UP);
+ mStartEvent = mStartBuilder.build();
+ mStopEvent = mStopBuilder.build();
+ }
+
+ @Test
+ public void testStartsBand() {
+ assertTrue(mBandController.shouldStart(mStartEvent));
+ }
+
+ @Test
+ public void testStartsBand_NoItems() {
+ mAdapter.updateTestModelIds(Collections.EMPTY_LIST);
+ assertTrue(mBandController.shouldStart(mStartEvent));
+ }
+
+ @Test
+ public void testBadStart_NoButtons() {
+ assertFalse(mBandController.shouldStart(
+ mStartBuilder.releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+ }
+
+ @Test
+ public void testBadStart_SecondaryButton() {
+ assertFalse(
+ mBandController.shouldStart(mStartBuilder.secondary().build()));
+ }
+
+ @Test
+ public void testBadStart_TertiaryButton() {
+ assertFalse(
+ mBandController.shouldStart(mStartBuilder.tertiary().build()));
+ }
+
+ @Test
+ public void testBadStart_Touch() {
+ assertFalse(mBandController.shouldStart(
+ mStartBuilder.touch().releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+ }
+
+ @Test
+ public void testBadStart_RespectsCanInitiateBand() {
+ mBandPredicate.setCanInitiate(false);
+ assertFalse(mBandController.shouldStart(mStartEvent));
+ }
+
+ @Test
+ public void testBadStart_ActionDown() {
+ assertFalse(mBandController
+ .shouldStart(mStartBuilder.action(MotionEvent.ACTION_DOWN).build()));
+ }
+
+ @Test
+ public void testBadStart_ActionUp() {
+ assertFalse(mBandController
+ .shouldStart(mStartBuilder.action(MotionEvent.ACTION_UP).build()));
+ }
+
+ @Test
+ public void testBadStart_ActionPointerDown() {
+ assertFalse(mBandController.shouldStart(
+ mStartBuilder.action(MotionEvent.ACTION_POINTER_DOWN).build()));
+ }
+
+ @Test
+ public void testBadStart_ActionPointerUp() {
+ assertFalse(mBandController.shouldStart(
+ mStartBuilder.action(MotionEvent.ACTION_POINTER_UP).build()));
+ }
+
+ @Test
+ public void testBadStart_alreadyActive() {
+ mIsActive = true;
+ assertFalse(mBandController.shouldStart(mStartEvent));
+ }
+
+ @Test
+ public void testGoodStop() {
+ mIsActive = true;
+ assertTrue(mBandController.shouldStop(mStopEvent));
+ }
+
+ @Test
+ public void testGoodStop_PointerUp() {
+ mIsActive = true;
+ assertTrue(mBandController
+ .shouldStop(mStopBuilder.action(MotionEvent.ACTION_POINTER_UP).build()));
+ }
+
+ @Test
+ public void testGoodStop_Cancel() {
+ mIsActive = true;
+ assertTrue(mBandController
+ .shouldStop(mStopBuilder.action(MotionEvent.ACTION_CANCEL).build()));
+ }
+
+ @Test
+ public void testBadStop_NotActive() {
+ assertFalse(mBandController.shouldStop(mStopEvent));
+ }
+
+ @Test
+ public void testBadStop_Move() {
+ mIsActive = true;
+ assertFalse(mBandController.shouldStop(
+ mStopBuilder.action(MotionEvent.ACTION_MOVE).touch().build()));
+ }
+
+ @Test
+ public void testBadStop_Down() {
+ mIsActive = true;
+ assertFalse(mBandController.shouldStop(
+ mStopBuilder.action(MotionEvent.ACTION_DOWN).touch().build()));
+ }
+
+ private final class TestBandHost extends BandSelectionHelper.BandHost<String> {
+ @Override
+ GridModel<String> createGridModel() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ void showBand(Rect rect) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ void hideBand() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ void addOnScrollListener(OnScrollListener listener) {
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java
new file mode 100644
index 0000000..b012f84
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentLockTest {
+
+ private ContentLock mLock = new ContentLock();
+ private boolean mCalled;
+
+ private Runnable mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mCalled = true;
+ }
+ };
+
+ @Before
+ public void setUp() {
+ mCalled = false;
+ }
+
+ @Test
+ public void testNotBlocking_callbackNotBlocked() {
+ mLock.runWhenUnlocked(mRunnable);
+ assertTrue(mCalled);
+ }
+
+ @Test
+ public void testToggleBlocking_callbackNotBlocked() {
+ mLock.block();
+ mLock.unblock();
+ mLock.runWhenUnlocked(mRunnable);
+ assertTrue(mCalled);
+ }
+
+ @Test
+ public void testBlocking_callbackBlocked() {
+ mLock.block();
+ mLock.runWhenUnlocked(mRunnable);
+ assertFalse(mCalled);
+ }
+
+ @Test
+ public void testBlockthenUnblock_callbackNotBlocked() {
+ mLock.block();
+ mLock.runWhenUnlocked(mRunnable);
+ mLock.unblock();
+ assertTrue(mCalled);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java
new file mode 100644
index 0000000..ddcd9a8
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseBooleanArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestSelectionObserver;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DefaultSelectionHelperTest {
+
+ private List<String> mItems;
+ private Set<String> mIgnored;
+ private TestAdapter mAdapter;
+ private DefaultSelectionHelper<String> mHelper;
+ private TestSelectionObserver<String> mListener;
+ private SelectionProbe mSelection;
+
+ @Before
+ public void setUp() throws Exception {
+ mIgnored = new HashSet<>();
+ mItems = TestAdapter.createItemList(100);
+ mListener = new TestSelectionObserver<>();
+ mAdapter = new TestAdapter();
+ mAdapter.updateTestModelIds(mItems);
+
+ SelectionPredicate selectionPredicate = new SelectionPredicate<String>() {
+
+ @Override
+ public boolean canSetStateForKey(String id, boolean nextState) {
+ return !nextState || !mIgnored.contains(id);
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ throw new UnsupportedOperationException("Not implemented.");
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return true;
+ }
+ };
+
+ ItemKeyProvider<String> keyProvider =
+ new TestItemKeyProvider<String>(ItemKeyProvider.SCOPE_MAPPED, mAdapter);
+ mHelper = new DefaultSelectionHelper<>(
+ keyProvider,
+ selectionPredicate);
+
+ EventBridge.install(mAdapter, mHelper, keyProvider);
+
+ mHelper.addObserver(mListener);
+
+ mSelection = new SelectionProbe(mHelper, mListener);
+
+ mIgnored.clear();
+ }
+
+ @Test
+ public void testSelect() {
+ mHelper.select(mItems.get(7));
+
+ mSelection.assertSelection(7);
+ }
+
+ @Test
+ public void testDeselect() {
+ mHelper.select(mItems.get(7));
+ mHelper.deselect(mItems.get(7));
+
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testSelection_DoNothingOnUnselectableItem() {
+ mIgnored.add(mItems.get(7));
+ boolean selected = mHelper.select(mItems.get(7));
+
+ assertFalse(selected);
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testSelect_NotifiesListenersOfChange() {
+ mHelper.select(mItems.get(7));
+
+ mListener.assertSelectionChanged();
+ }
+
+ @Test
+ public void testSelect_NotifiesAdapterOfSelect() {
+ mHelper.select(mItems.get(7));
+
+ mAdapter.assertNotifiedOfSelectionChange(7);
+ }
+
+ @Test
+ public void testSelect_NotifiesAdapterOfDeselect() {
+ mHelper.select(mItems.get(7));
+ mAdapter.resetSelectionNotifications();
+ mHelper.deselect(mItems.get(7));
+ mAdapter.assertNotifiedOfSelectionChange(7);
+ }
+
+ @Test
+ public void testDeselect_NotifiesSelectionChanged() {
+ mHelper.select(mItems.get(7));
+ mHelper.deselect(mItems.get(7));
+
+ mListener.assertSelectionChanged();
+ }
+
+ @Test
+ public void testSelection_PersistsOnUpdate() {
+ mHelper.select(mItems.get(7));
+ mAdapter.updateTestModelIds(mItems);
+
+ mSelection.assertSelection(7);
+ }
+
+ @Test
+ public void testSetItemsSelected() {
+ mHelper.setItemsSelected(getStringIds(6, 7, 8), true);
+
+ mSelection.assertRangeSelected(6, 8);
+ }
+
+ @Test
+ public void testSetItemsSelected_SkipUnselectableItem() {
+ mIgnored.add(mItems.get(7));
+
+ mHelper.setItemsSelected(getStringIds(6, 7, 8), true);
+
+ mSelection.assertSelected(6);
+ mSelection.assertNotSelected(7);
+ mSelection.assertSelected(8);
+ }
+
+ @Test
+ public void testClear_RemovesPrimarySelection() {
+ mHelper.select(mItems.get(1));
+ mHelper.select(mItems.get(2));
+ mHelper.clear();
+
+ assertFalse(mHelper.hasSelection());
+ }
+
+ @Test
+ public void testClear_RemovesProvisionalSelection() {
+ Set<String> prov = new HashSet<>();
+ prov.add(mItems.get(1));
+ prov.add(mItems.get(2));
+ mHelper.clear();
+ // if there is a provisional selection, convert it to regular selection so we can poke it.
+ mHelper.mergeProvisionalSelection();
+
+ assertFalse(mHelper.hasSelection());
+ }
+
+ @Test
+ public void testRangeSelection() {
+ mHelper.startRange(15);
+ mHelper.extendRange(19);
+ mSelection.assertRangeSelection(15, 19);
+ }
+
+ @Test
+ public void testRangeSelection_SkipUnselectableItem() {
+ mIgnored.add(mItems.get(17));
+
+ mHelper.startRange(15);
+ mHelper.extendRange(19);
+
+ mSelection.assertRangeSelected(15, 16);
+ mSelection.assertNotSelected(17);
+ mSelection.assertRangeSelected(18, 19);
+ }
+
+ @Test
+ public void testRangeSelection_snapExpand() {
+ mHelper.startRange(15);
+ mHelper.extendRange(19);
+ mHelper.extendRange(27);
+ mSelection.assertRangeSelection(15, 27);
+ }
+
+ @Test
+ public void testRangeSelection_snapContract() {
+ mHelper.startRange(15);
+ mHelper.extendRange(27);
+ mHelper.extendRange(19);
+ mSelection.assertRangeSelection(15, 19);
+ }
+
+ @Test
+ public void testRangeSelection_snapInvert() {
+ mHelper.startRange(15);
+ mHelper.extendRange(27);
+ mHelper.extendRange(3);
+ mSelection.assertRangeSelection(3, 15);
+ }
+
+ @Test
+ public void testRangeSelection_multiple() {
+ mHelper.startRange(15);
+ mHelper.extendRange(27);
+ mHelper.endRange();
+ mHelper.startRange(42);
+ mHelper.extendRange(57);
+ mSelection.assertSelectionSize(29);
+ mSelection.assertRangeSelected(15, 27);
+ mSelection.assertRangeSelected(42, 57);
+ }
+
+ @Test
+ public void testProvisionalRangeSelection() {
+ mHelper.startRange(13);
+ mHelper.extendProvisionalRange(15);
+ mSelection.assertRangeSelection(13, 15);
+ mHelper.getSelection().mergeProvisionalSelection();
+ mHelper.endRange();
+ mSelection.assertSelectionSize(3);
+ }
+
+ @Test
+ public void testProvisionalRangeSelection_endEarly() {
+ mHelper.startRange(13);
+ mHelper.extendProvisionalRange(15);
+ mSelection.assertRangeSelection(13, 15);
+
+ mHelper.endRange();
+ // If we end range selection prematurely for provision selection, nothing should be selected
+ // except the first item
+ mSelection.assertSelectionSize(1);
+ }
+
+ @Test
+ public void testProvisionalRangeSelection_snapExpand() {
+ mHelper.startRange(13);
+ mHelper.extendProvisionalRange(15);
+ mSelection.assertRangeSelection(13, 15);
+ mHelper.getSelection().mergeProvisionalSelection();
+ mHelper.extendRange(18);
+ mSelection.assertRangeSelection(13, 18);
+ }
+
+ @Test
+ public void testCombinationRangeSelection_IntersectsOldSelection() {
+ mHelper.startRange(13);
+ mHelper.extendRange(15);
+ mSelection.assertRangeSelection(13, 15);
+
+ mHelper.startRange(11);
+ mHelper.extendProvisionalRange(18);
+ mSelection.assertRangeSelected(11, 18);
+ mHelper.endRange();
+ mSelection.assertRangeSelected(13, 15);
+ mSelection.assertRangeSelected(11, 11);
+ mSelection.assertSelectionSize(4);
+ }
+
+ @Test
+ public void testProvisionalSelection() {
+ Selection s = mHelper.getSelection();
+ mSelection.assertNoSelection();
+
+ // Mimicking band selection case -- BandController notifies item callback by itself.
+ mListener.onItemStateChanged(mItems.get(1), true);
+ mListener.onItemStateChanged(mItems.get(2), true);
+
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(1, true);
+ provisional.append(2, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ mSelection.assertSelection(1, 2);
+ }
+
+ @Test
+ public void testProvisionalSelection_Replace() {
+ Selection s = mHelper.getSelection();
+
+ // Mimicking band selection case -- BandController notifies item callback by itself.
+ mListener.onItemStateChanged(mItems.get(1), true);
+ mListener.onItemStateChanged(mItems.get(2), true);
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(1, true);
+ provisional.append(2, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+
+ mListener.onItemStateChanged(mItems.get(1), false);
+ mListener.onItemStateChanged(mItems.get(2), false);
+ provisional.clear();
+
+ mListener.onItemStateChanged(mItems.get(3), true);
+ mListener.onItemStateChanged(mItems.get(4), true);
+ provisional.append(3, true);
+ provisional.append(4, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ mSelection.assertSelection(3, 4);
+ }
+
+ @Test
+ public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
+ Selection s = mHelper.getSelection();
+
+ // Mimicking band selection case -- BandController notifies item callback by itself.
+ mListener.onItemStateChanged(mItems.get(1), true);
+ mListener.onItemStateChanged(mItems.get(2), true);
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(1, true);
+ provisional.append(2, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+
+ mListener.onItemStateChanged(mItems.get(1), false);
+ mListener.onItemStateChanged(mItems.get(2), false);
+ provisional.clear();
+
+ mListener.onItemStateChanged(mItems.get(1), true);
+ provisional.append(1, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ mSelection.assertSelection(1);
+ }
+
+ @Test
+ public void testProvisionalSelection_Apply() {
+ Selection s = mHelper.getSelection();
+
+ // Mimicking band selection case -- BandController notifies item callback by itself.
+ mListener.onItemStateChanged(mItems.get(1), true);
+ mListener.onItemStateChanged(mItems.get(2), true);
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(1, true);
+ provisional.append(2, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ s.mergeProvisionalSelection();
+
+ mSelection.assertSelection(1, 2);
+ }
+
+ @Test
+ public void testProvisionalSelection_Cancel() {
+ mHelper.select(mItems.get(1));
+ mHelper.select(mItems.get(2));
+ Selection s = mHelper.getSelection();
+
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(3, true);
+ provisional.append(4, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ s.clearProvisionalSelection();
+
+ // Original selection should remain.
+ mSelection.assertSelection(1, 2);
+ }
+
+ @Test
+ public void testProvisionalSelection_IntersectsAppliedSelection() {
+ mHelper.select(mItems.get(1));
+ mHelper.select(mItems.get(2));
+ Selection s = mHelper.getSelection();
+
+ // Mimicking band selection case -- BandController notifies item callback by itself.
+ mListener.onItemStateChanged(mItems.get(3), true);
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(2, true);
+ provisional.append(3, true);
+ s.setProvisionalSelection(getItemIds(provisional));
+ mSelection.assertSelection(1, 2, 3);
+ }
+
+ private Set<String> getItemIds(SparseBooleanArray selection) {
+ Set<String> ids = new HashSet<>();
+
+ int count = selection.size();
+ for (int i = 0; i < count; ++i) {
+ ids.add(mItems.get(selection.keyAt(i)));
+ }
+
+ return ids;
+ }
+
+ @Test
+ public void testObserverOnChanged_NotifiesListenersOfChange() {
+ mAdapter.notifyDataSetChanged();
+
+ mListener.assertSelectionChanged();
+ }
+
+ private Iterable<String> getStringIds(int... ids) {
+ List<String> stringIds = new ArrayList<>(ids.length);
+ for (int id : ids) {
+ stringIds.add(mItems.get(id));
+ }
+ return stringIds;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java
new file mode 100644
index 0000000..02527b1
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestSelectionObserver;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DefaultSelectionHelper_SingleSelectTest {
+
+ private List<String> mItems;
+ private SelectionHelper<String> mHelper;
+ private TestSelectionObserver<String> mListener;
+ private SelectionProbe mSelection;
+
+ @Before
+ public void setUp() throws Exception {
+ mItems = TestAdapter.createItemList(100);
+ mListener = new TestSelectionObserver<>();
+ TestAdapter adapter = new TestAdapter();
+ adapter.updateTestModelIds(mItems);
+
+ ItemKeyProvider<String> keyProvider =
+ new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, adapter);
+ mHelper = new DefaultSelectionHelper<>(
+ keyProvider,
+ SelectionPredicates.selectSingleAnything());
+ EventBridge.install(adapter, mHelper, keyProvider);
+
+ mHelper.addObserver(mListener);
+
+ mSelection = new SelectionProbe(mHelper);
+ }
+
+ @Test
+ public void testSimpleSelect() {
+ mHelper.select(mItems.get(3));
+ mHelper.select(mItems.get(4));
+ mListener.assertSelectionChanged();
+ mSelection.assertSelection(4);
+ }
+
+ @Test
+ public void testRangeSelectionNotEstablished() {
+ mHelper.select(mItems.get(3));
+ mListener.reset();
+
+ try {
+ mHelper.extendRange(10);
+ fail("Should have thrown.");
+ } catch (Exception expected) { }
+
+ mListener.assertSelectionUnchanged();
+ mSelection.assertSelection(3);
+ }
+
+ @Test
+ public void testProvisionalRangeSelection_Ignored() {
+ mHelper.startRange(13);
+ mHelper.extendProvisionalRange(15);
+ mSelection.assertSelection(13);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java
new file mode 100644
index 0000000..0e5a5a9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyFloat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import androidx.recyclerview.selection.testing.TestEvents.Mouse;
+import androidx.recyclerview.selection.testing.TestEvents.Touch;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class GestureRouterTest {
+
+ private TestHandler mHandler;
+ private TestHandler mAlt;
+ private GestureRouter<TestHandler> mRouter;
+
+ @Before
+ public void setUp() {
+ mAlt = new TestHandler();
+ mHandler = new TestHandler();
+ }
+
+ @Test
+ public void testDelegates() {
+ mRouter = new GestureRouter<>();
+ mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+ mRouter.register(MotionEvent.TOOL_TYPE_FINGER, mAlt);
+
+ mRouter.onDown(Mouse.CLICK);
+ mHandler.assertCalled_onDown(Mouse.CLICK);
+ mAlt.assertNotCalled_onDown();
+
+ mRouter.onShowPress(Mouse.CLICK);
+ mHandler.assertCalled_onShowPress(Mouse.CLICK);
+ mAlt.assertNotCalled_onShowPress();
+
+ mRouter.onSingleTapUp(Mouse.CLICK);
+ mHandler.assertCalled_onSingleTapUp(Mouse.CLICK);
+ mAlt.assertNotCalled_onSingleTapUp();
+
+ mRouter.onScroll(null, Mouse.CLICK, -1, -1);
+ mHandler.assertCalled_onScroll(null, Mouse.CLICK, -1, -1);
+ mAlt.assertNotCalled_onScroll();
+
+ mRouter.onLongPress(Mouse.CLICK);
+ mHandler.assertCalled_onLongPress(Mouse.CLICK);
+ mAlt.assertNotCalled_onLongPress();
+
+ mRouter.onFling(null, Mouse.CLICK, -1, -1);
+ mHandler.assertCalled_onFling(null, Mouse.CLICK, -1, -1);
+ mAlt.assertNotCalled_onFling();
+
+ mRouter.onSingleTapConfirmed(Mouse.CLICK);
+ mHandler.assertCalled_onSingleTapConfirmed(Mouse.CLICK);
+ mAlt.assertNotCalled_onSingleTapConfirmed();
+
+ mRouter.onDoubleTap(Mouse.CLICK);
+ mHandler.assertCalled_onDoubleTap(Mouse.CLICK);
+ mAlt.assertNotCalled_onDoubleTap();
+
+ mRouter.onDoubleTapEvent(Mouse.CLICK);
+ mHandler.assertCalled_onDoubleTapEvent(Mouse.CLICK);
+ mAlt.assertNotCalled_onDoubleTapEvent();
+ }
+
+ @Test
+ public void testFallsback() {
+ mRouter = new GestureRouter<>(mAlt);
+ mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+
+ mRouter.onDown(Touch.TAP);
+ mAlt.assertCalled_onDown(Touch.TAP);
+
+ mRouter.onShowPress(Touch.TAP);
+ mAlt.assertCalled_onShowPress(Touch.TAP);
+
+ mRouter.onSingleTapUp(Touch.TAP);
+ mAlt.assertCalled_onSingleTapUp(Touch.TAP);
+
+ mRouter.onScroll(null, Touch.TAP, -1, -1);
+ mAlt.assertCalled_onScroll(null, Touch.TAP, -1, -1);
+
+ mRouter.onLongPress(Touch.TAP);
+ mAlt.assertCalled_onLongPress(Touch.TAP);
+
+ mRouter.onFling(null, Touch.TAP, -1, -1);
+ mAlt.assertCalled_onFling(null, Touch.TAP, -1, -1);
+
+ mRouter.onSingleTapConfirmed(Touch.TAP);
+ mAlt.assertCalled_onSingleTapConfirmed(Touch.TAP);
+
+ mRouter.onDoubleTap(Touch.TAP);
+ mAlt.assertCalled_onDoubleTap(Touch.TAP);
+
+ mRouter.onDoubleTapEvent(Touch.TAP);
+ mAlt.assertCalled_onDoubleTapEvent(Touch.TAP);
+ }
+
+ @Test
+ public void testEatsEventsWhenNoFallback() {
+ mRouter = new GestureRouter<>();
+ // Register the the delegate on mouse so touch events don't get handled.
+ mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+
+ mRouter.onDown(Touch.TAP);
+ mAlt.assertNotCalled_onDown();
+
+ mRouter.onShowPress(Touch.TAP);
+ mAlt.assertNotCalled_onShowPress();
+
+ mRouter.onSingleTapUp(Touch.TAP);
+ mAlt.assertNotCalled_onSingleTapUp();
+
+ mRouter.onScroll(null, Touch.TAP, -1, -1);
+ mAlt.assertNotCalled_onScroll();
+
+ mRouter.onLongPress(Touch.TAP);
+ mAlt.assertNotCalled_onLongPress();
+
+ mRouter.onFling(null, Touch.TAP, -1, -1);
+ mAlt.assertNotCalled_onFling();
+
+ mRouter.onSingleTapConfirmed(Touch.TAP);
+ mAlt.assertNotCalled_onSingleTapConfirmed();
+
+ mRouter.onDoubleTap(Touch.TAP);
+ mAlt.assertNotCalled_onDoubleTap();
+
+ mRouter.onDoubleTapEvent(Touch.TAP);
+ mAlt.assertNotCalled_onDoubleTapEvent();
+ }
+
+ private static final class TestHandler implements OnGestureListener, OnDoubleTapListener {
+
+ private final Spy mSpy = Mockito.mock(Spy.class);
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return mSpy.onDown(e);
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ mSpy.onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mSpy.onSingleTapUp(e);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return mSpy.onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ mSpy.onLongPress(e);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return mSpy.onFling(e1, e2, velocityX, velocityY);
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return mSpy.onSingleTapConfirmed(e);
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mSpy.onDoubleTap(e);
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ return mSpy.onDoubleTapEvent(e);
+ }
+
+ void assertCalled_onDown(MotionEvent e) {
+ verify(mSpy).onDown(e);
+ }
+
+ void assertCalled_onShowPress(MotionEvent e) {
+ verify(mSpy).onShowPress(e);
+ }
+
+ void assertCalled_onSingleTapUp(MotionEvent e) {
+ verify(mSpy).onSingleTapUp(e);
+ }
+
+ void assertCalled_onScroll(MotionEvent e1, MotionEvent e2, float x, float y) {
+ verify(mSpy).onScroll(e1, e2, x, y);
+ }
+
+ void assertCalled_onLongPress(MotionEvent e) {
+ verify(mSpy).onLongPress(e);
+ }
+
+ void assertCalled_onFling(MotionEvent e1, MotionEvent e2, float x, float y) {
+ Mockito.verify(mSpy).onFling(e1, e2, x, y);
+ }
+
+ void assertCalled_onSingleTapConfirmed(MotionEvent e) {
+ Mockito.verify(mSpy).onSingleTapConfirmed(e);
+ }
+
+ void assertCalled_onDoubleTap(MotionEvent e) {
+ Mockito.verify(mSpy).onDoubleTap(e);
+ }
+
+ void assertCalled_onDoubleTapEvent(MotionEvent e) {
+ Mockito.verify(mSpy).onDoubleTapEvent(e);
+ }
+
+ void assertNotCalled_onDown() {
+ verify(mSpy, never()).onDown((MotionEvent) any());
+ }
+
+ void assertNotCalled_onShowPress() {
+ verify(mSpy, never()).onShowPress((MotionEvent) any());
+ }
+
+ void assertNotCalled_onSingleTapUp() {
+ verify(mSpy, never()).onSingleTapUp((MotionEvent) any());
+ }
+
+ void assertNotCalled_onScroll() {
+ verify(mSpy, never()).onScroll(
+ (MotionEvent) any(), (MotionEvent) any(), anyFloat(), anyFloat());
+ }
+
+ void assertNotCalled_onLongPress() {
+ verify(mSpy, never()).onLongPress((MotionEvent) any());
+ }
+
+ void assertNotCalled_onFling() {
+ Mockito.verify(mSpy, never()).onFling(
+ (MotionEvent) any(), (MotionEvent) any(), anyFloat(), anyFloat());
+ }
+
+ void assertNotCalled_onSingleTapConfirmed() {
+ Mockito.verify(mSpy, never()).onSingleTapConfirmed((MotionEvent) any());
+ }
+
+ void assertNotCalled_onDoubleTap() {
+ Mockito.verify(mSpy, never()).onDoubleTap((MotionEvent) any());
+ }
+
+ void assertNotCalled_onDoubleTapEvent() {
+ Mockito.verify(mSpy, never()).onDoubleTapEvent((MotionEvent) any());
+ }
+ }
+
+ private interface Spy extends OnGestureListener, OnDoubleTapListener {
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java
new file mode 100644
index 0000000..f1e38d9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAutoScroller;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GestureSelectionHelperTest {
+
+ private static final List<String> ITEMS = TestData.createStringData(100);
+ private static final MotionEvent DOWN = TestEvents.builder()
+ .action(MotionEvent.ACTION_DOWN)
+ .location(1, 1)
+ .build();
+
+ private static final MotionEvent MOVE = TestEvents.builder()
+ .action(MotionEvent.ACTION_MOVE)
+ .location(1, 1)
+ .build();
+
+ private static final MotionEvent UP = TestEvents.builder()
+ .action(MotionEvent.ACTION_UP)
+ .location(1, 1)
+ .build();
+
+ private GestureSelectionHelper mHelper;
+ private SelectionHelper<String> mSelectionHelper;
+ private SelectionProbe mSelection;
+ private ContentLock mLock;
+ private TestViewDelegate mView;
+
+ @Before
+ public void setUp() {
+ mSelectionHelper = SelectionHelpers.createTestInstance(ITEMS);
+ mSelection = new SelectionProbe(mSelectionHelper);
+ mLock = new ContentLock();
+ mView = new TestViewDelegate();
+ mHelper = new GestureSelectionHelper(
+ mSelectionHelper, mView, new TestAutoScroller(), mLock);
+ }
+
+ @Test
+ public void testIgnoresDownOnNoPosition() {
+ mView.mNextPosition = RecyclerView.NO_POSITION;
+ assertFalse(mHelper.onInterceptTouchEvent(null, DOWN));
+ }
+
+
+ @Test
+ public void testNoStartOnIllegalPosition() {
+ mHelper.onInterceptTouchEvent(null, DOWN);
+ try {
+ mHelper.start();
+ fail("Should have thrown.");
+ } catch (Exception expected) {
+ }
+ }
+
+ @Test
+ public void testClaimsDownOnItem() {
+ mView.mNextPosition = 0;
+ assertTrue(mHelper.onInterceptTouchEvent(null, DOWN));
+ }
+
+ @Test
+ public void testClaimsMoveIfStarted() {
+ mView.mNextPosition = 0;
+ assertTrue(mHelper.onInterceptTouchEvent(null, DOWN));
+
+ // Normally, this is controller by the TouchSelectionHelper via a a long press gesture.
+ mSelectionHelper.select("1");
+ mSelectionHelper.anchorRange(1);
+ mHelper.start();
+ assertTrue(mHelper.onInterceptTouchEvent(null, MOVE));
+ }
+
+ @Test
+ public void testCreatesRangeSelection() {
+ mView.mNextPosition = 1;
+ mHelper.onInterceptTouchEvent(null, DOWN);
+ // Another way we are implicitly coupled to TouchInputHandler, is that we depend on
+ // long press to establish the initial anchor point. Without that we'll get an
+ // error when we try to extend the range.
+
+ mSelectionHelper.select("1");
+ mSelectionHelper.anchorRange(1);
+ mHelper.start();
+
+ mHelper.onTouchEvent(null, MOVE);
+
+ mView.mNextPosition = 9;
+ mHelper.onTouchEvent(null, MOVE);
+ mHelper.onTouchEvent(null, UP);
+
+ mSelection.assertRangeSelected(1, 9);
+ }
+
+ private static final class TestViewDelegate extends GestureSelectionHelper.ViewDelegate {
+
+ private int mNextPosition = RecyclerView.NO_POSITION;
+
+ @Override
+ int getHeight() {
+ return 1000;
+ }
+
+ @Override
+ int getItemUnder(MotionEvent e) {
+ return mNextPosition;
+ }
+
+ @Override
+ int getLastGlidedItemPosition(MotionEvent e) {
+ return mNextPosition;
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java
new file mode 100644
index 0000000..7e5251a
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.recyclerview.selection.GestureSelectionHelper.RecyclerViewDelegate;
+import androidx.recyclerview.selection.testing.TestEvents;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GestureSelectionHelper_RecyclerViewDelegateTest {
+
+ // Simulate a (20, 20) box locating at (20, 20)
+ static final int LEFT_BORDER = 20;
+ static final int RIGHT_BORDER = 40;
+ static final int TOP_BORDER = 20;
+ static final int BOTTOM_BORDER = 40;
+
+ @Test
+ public void testLtrPastLastItem() {
+ MotionEvent event = createEvent(100, 100);
+ assertTrue(RecyclerViewDelegate.isPastLastItem(
+ TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_LTR));
+ }
+
+ @Test
+ public void testLtrPastLastItem_Inverse() {
+ MotionEvent event = createEvent(10, 10);
+ assertFalse(RecyclerViewDelegate.isPastLastItem(
+ TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_LTR));
+ }
+
+ @Test
+ public void testRtlPastLastItem() {
+ MotionEvent event = createEvent(10, 30);
+ assertTrue(RecyclerViewDelegate.isPastLastItem(
+ TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_RTL));
+ }
+
+ @Test
+ public void testRtlPastLastItem_Inverse() {
+ MotionEvent event = createEvent(100, 100);
+ assertFalse(RecyclerViewDelegate.isPastLastItem(
+ TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_RTL));
+ }
+
+ private static MotionEvent createEvent(int x, int y) {
+ return TestEvents.builder().action(MotionEvent.ACTION_MOVE).location(x, y).build();
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java
new file mode 100644
index 0000000..8924c52
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static androidx.recyclerview.selection.GridModel.NOT_SET;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GridModelTest {
+
+ private static final int VIEW_PADDING_PX = 5;
+ private static final int CHILD_VIEW_EDGE_PX = 100;
+ private static final int VIEWPORT_HEIGHT = 500;
+
+ private GridModel<String> mModel;
+ private TestHost mHost;
+ private TestAdapter mAdapter;
+ private Set<String> mLastSelection;
+ private int mViewWidth;
+
+ // TLDR: Don't call model.{start|resize}Selection; use the local #startSelection and
+ // #resizeSelection methods instead.
+ //
+ // The reason for this is that selection is stateful and involves operations that take the
+ // current UI state (e.g scrolling) into account. This test maintains its own copy of the
+ // selection bounds as control data for verifying selections. Keep this data in sync by calling
+ // #startSelection and
+ // #resizeSelection.
+ private Point mSelectionOrigin;
+ private Point mSelectionPoint;
+
+ @After
+ public void tearDown() {
+ mModel = null;
+ mHost = null;
+ mLastSelection = null;
+ }
+
+ @Test
+ public void testSelectionLeftOfItems() {
+ initData(20, 5);
+ startSelection(new Point(0, 10));
+ resizeSelection(new Point(1, 11));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testSelectionRightOfItems() {
+ initData(20, 4);
+ startSelection(new Point(mViewWidth - 1, 10));
+ resizeSelection(new Point(mViewWidth - 2, 11));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testSelectionAboveItems() {
+ initData(20, 4);
+ startSelection(new Point(10, 0));
+ resizeSelection(new Point(11, 1));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testSelectionBelowItems() {
+ initData(5, 4);
+ startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
+ resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testVerticalSelectionBetweenItems() {
+ initData(20, 4);
+ startSelection(new Point(106, 0));
+ resizeSelection(new Point(107, 200));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testHorizontalSelectionBetweenItems() {
+ initData(20, 4);
+ startSelection(new Point(0, 105));
+ resizeSelection(new Point(200, 106));
+ assertNoSelection();
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testGrowingAndShrinkingSelection() {
+ initData(20, 4);
+ startSelection(new Point(0, 0));
+
+ resizeSelection(new Point(5, 5));
+ verifySelection();
+
+ resizeSelection(new Point(109, 109));
+ verifySelection();
+
+ resizeSelection(new Point(110, 109));
+ verifySelection();
+
+ resizeSelection(new Point(110, 110));
+ verifySelection();
+
+ resizeSelection(new Point(214, 214));
+ verifySelection();
+
+ resizeSelection(new Point(215, 214));
+ verifySelection();
+
+ resizeSelection(new Point(214, 214));
+ verifySelection();
+
+ resizeSelection(new Point(110, 110));
+ verifySelection();
+
+ resizeSelection(new Point(110, 109));
+ verifySelection();
+
+ resizeSelection(new Point(109, 109));
+ verifySelection();
+
+ resizeSelection(new Point(5, 5));
+ verifySelection();
+
+ resizeSelection(new Point(0, 0));
+ verifySelection();
+
+ assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testSelectionMovingAroundOrigin() {
+ initData(16, 4);
+
+ startSelection(new Point(210, 210));
+ resizeSelection(new Point(mViewWidth - 1, 0));
+ verifySelection();
+
+ resizeSelection(new Point(0, 0));
+ verifySelection();
+
+ resizeSelection(new Point(0, 420));
+ verifySelection();
+
+ resizeSelection(new Point(mViewWidth - 1, 420));
+ verifySelection();
+
+ // This is manually figured and will need to be adjusted if the separator position is
+ // changed.
+ assertEquals(7, mModel.getPositionNearestOrigin());
+ }
+
+ @Test
+ public void testScrollingBandSelect() {
+ initData(40, 4);
+
+ startSelection(new Point(0, 0));
+ resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+ verifySelection();
+
+ scroll(CHILD_VIEW_EDGE_PX);
+ verifySelection();
+
+ resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
+ verifySelection();
+
+ scroll(CHILD_VIEW_EDGE_PX);
+ verifySelection();
+
+ scroll(-2 * CHILD_VIEW_EDGE_PX);
+ verifySelection();
+
+ resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+ verifySelection();
+
+ assertEquals(0, mModel.getPositionNearestOrigin());
+ }
+
+ private void initData(final int numChildren, int numColumns) {
+ mHost = new TestHost(numChildren, numColumns);
+ mAdapter = new TestAdapter() {
+ @Override
+ public String getSelectionKey(int position) {
+ return Integer.toString(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return numChildren;
+ }
+ };
+
+ mViewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
+
+ mModel = new GridModel<String>(
+ mHost,
+ new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, mAdapter),
+ SelectionPredicates.<String>selectAnything());
+
+ mModel.addOnSelectionChangedListener(
+ new GridModel.SelectionObserver<String>() {
+ @Override
+ public void onSelectionChanged(Set<String> updatedSelection) {
+ mLastSelection = updatedSelection;
+ }
+ });
+ }
+
+ /** Returns the current selection area as a Rect. */
+ private Rect getSelectionArea() {
+ // Construct a rect from the two selection points.
+ Rect selectionArea = new Rect(
+ mSelectionOrigin.x, mSelectionOrigin.y, mSelectionOrigin.x, mSelectionOrigin.y);
+ selectionArea.union(mSelectionPoint.x, mSelectionPoint.y);
+ // Rect intersection tests are exclusive of bounds, while the MSM's selection code is
+ // inclusive. Expand the rect by 1 pixel in all directions to account for this.
+ selectionArea.inset(-1, -1);
+
+ return selectionArea;
+ }
+
+ /** Asserts that the selection is currently empty. */
+ private void assertNoSelection() {
+ assertEquals("Unexpected mItems " + mLastSelection + " in selection " + getSelectionArea(),
+ 0, mLastSelection.size());
+ }
+
+ /** Verifies the selection using actual bbox checks. */
+ private void verifySelection() {
+ Rect selectionArea = getSelectionArea();
+ for (TestHost.Item item: mHost.mItems) {
+ if (Rect.intersects(selectionArea, item.rect)) {
+ assertTrue("Expected item " + item + " was not in selection " + selectionArea,
+ mLastSelection.contains(item.name));
+ } else {
+ assertFalse("Unexpected item " + item + " in selection" + selectionArea,
+ mLastSelection.contains(item.name));
+ }
+ }
+ }
+
+ private void startSelection(Point p) {
+ mModel.startCapturing(p);
+ mSelectionOrigin = mHost.createAbsolutePoint(p);
+ }
+
+ private void resizeSelection(Point p) {
+ mModel.resizeSelection(p);
+ mSelectionPoint = mHost.createAbsolutePoint(p);
+ }
+
+ private void scroll(int dy) {
+ assertTrue(mHost.verticalOffset + VIEWPORT_HEIGHT + dy <= mHost.getTotalHeight());
+ mHost.verticalOffset += dy;
+ // Correct the cached selection point as well.
+ mSelectionPoint.y += dy;
+ mHost.mScrollListener.onScrolled(null, 0, dy);
+ }
+
+ private static final class TestHost extends GridModel.GridHost<String> {
+
+ private final int mNumColumns;
+ private final int mNumRows;
+ private final int mNumChildren;
+ private final int mSeparatorPosition;
+
+ public int horizontalOffset = 0;
+ public int verticalOffset = 0;
+ private List<Item> mItems = new ArrayList<>();
+
+ // Installed by GridModel on construction.
+ private @Nullable OnScrollListener mScrollListener;
+
+ TestHost(int numChildren, int numColumns) {
+ mNumChildren = numChildren;
+ mNumColumns = numColumns;
+ mSeparatorPosition = mNumColumns + 1;
+ mNumRows = setupGrid();
+ }
+
+ private int setupGrid() {
+ // Split the input set into folders and documents. Do this such that there is a
+ // partially-populated row in the middle of the grid, to test corner cases in layout
+ // code.
+ int y = VIEW_PADDING_PX;
+ int i = 0;
+ int numRows = 0;
+ while (i < mNumChildren) {
+ int top = y;
+ int height = CHILD_VIEW_EDGE_PX;
+ int width = CHILD_VIEW_EDGE_PX;
+ for (int j = 0; j < mNumColumns && i < mNumChildren; j++) {
+ int left = VIEW_PADDING_PX + (j * (width + VIEW_PADDING_PX));
+ mItems.add(new Item(
+ Integer.toString(i),
+ new Rect(
+ left,
+ top,
+ left + width - 1,
+ top + height - 1)));
+
+ // Create a partially populated row at the separator position.
+ if (++i == mSeparatorPosition) {
+ break;
+ }
+ }
+ y += height + VIEW_PADDING_PX;
+ numRows++;
+ }
+
+ return numRows;
+ }
+
+ private int getTotalHeight() {
+ return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
+ }
+
+ private int getFirstVisibleRowIndex() {
+ return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+ }
+
+ private int getLastVisibleRowIndex() {
+ int lastVisibleRowUncapped =
+ (VIEWPORT_HEIGHT + verticalOffset - 1) / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+ return Math.min(lastVisibleRowUncapped, mNumRows - 1);
+ }
+
+ private int getNumItemsInRow(int index) {
+ assertTrue(index >= 0 && index < mNumRows);
+ int mod = mSeparatorPosition % mNumColumns;
+ if (index == (mSeparatorPosition / mNumColumns)) {
+ // The row containing the separator may be incomplete
+ return mod > 0 ? mod : mNumColumns;
+ }
+ // Account for the partial separator row in the final row tally.
+ if (index == mNumRows - 1) {
+ // The last row may be incomplete
+ int finalRowCount = (mNumChildren - mod) % mNumColumns;
+ return finalRowCount > 0 ? finalRowCount : mNumColumns;
+ }
+
+ return mNumColumns;
+ }
+
+ @Override
+ public GridModel<String> createGridModel() {
+ throw new UnsupportedOperationException("Not implemented.");
+ }
+
+ @Override
+ public void addOnScrollListener(OnScrollListener listener) {
+ mScrollListener = listener;
+ }
+
+ @Override
+ public void removeOnScrollListener(OnScrollListener listener) {}
+
+ @Override
+ public Point createAbsolutePoint(Point relativePoint) {
+ return new Point(
+ relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
+ }
+
+ @Override
+ public int getVisibleChildCount() {
+ int childCount = 0;
+ for (int i = getFirstVisibleRowIndex(); i <= getLastVisibleRowIndex(); i++) {
+ childCount += getNumItemsInRow(i);
+ }
+ return childCount;
+ }
+
+ @Override
+ public int getAdapterPositionAt(int index) {
+ // Account for partial rows by actually tallying up the mItems in hidden rows.
+ int hiddenCount = 0;
+ for (int i = 0; i < getFirstVisibleRowIndex(); i++) {
+ hiddenCount += getNumItemsInRow(i);
+ }
+ return index + hiddenCount;
+ }
+
+ @Override
+ public Rect getAbsoluteRectForChildViewAt(int index) {
+ int adapterPosition = getAdapterPositionAt(index);
+ return mItems.get(adapterPosition).rect;
+ }
+
+ @Override
+ public int getColumnCount() {
+ return mNumColumns;
+ }
+
+ @Override
+ public void showBand(Rect rect) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void hideBand() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasView(int adapterPosition) {
+ return true;
+ }
+
+ public static final class Item {
+ public String name;
+ public Rect rect;
+
+ Item(String n, Rect r) {
+ name = n;
+ rect = r;
+ }
+
+ @Override
+ public String toString() {
+ return name + ": " + rect;
+ }
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java
new file mode 100644
index 0000000..b0e7276
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.ALT_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CTRL_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SECONDARY_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SHIFT_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.TERTIARY_CLICK;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetails;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestMouseCallbacks;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class MouseInputHandlerTest {
+
+ private static final List<String> ITEMS = TestData.createStringData(100);
+
+ private MouseInputHandler mInputDelegate;
+
+ private TestMouseCallbacks mMouseCallbacks;
+ private TestActivationCallbacks mActivationCallbacks;
+ private TestFocusCallbacks mFocusCallbacks;
+
+ private TestItemDetailsLookup mDetailsLookup;
+ private SelectionProbe mSelection;
+ private SelectionHelper mSelectionMgr;
+
+ private TestEvents.Builder mEvent;
+
+ @Before
+ public void setUp() {
+
+ mSelectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+ mDetailsLookup = new TestItemDetailsLookup();
+ mSelection = new SelectionProbe(mSelectionMgr);
+
+ mMouseCallbacks = new TestMouseCallbacks();
+ mActivationCallbacks = new TestActivationCallbacks();
+ mFocusCallbacks = new TestFocusCallbacks();
+
+ mInputDelegate = new MouseInputHandler(
+ mSelectionMgr,
+ new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+ mDetailsLookup,
+ mMouseCallbacks,
+ mActivationCallbacks,
+ mFocusCallbacks);
+
+ mEvent = TestEvents.builder().mouse();
+ mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+ }
+
+ @Test
+ public void testConfirmedClick_StartsSelection() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mSelection.assertSelection(11);
+ }
+
+ @Test
+ public void testClickOnSelectRegion_AddsToSelection() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(10).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapUp(CLICK);
+
+ mSelection.assertSelected(10, 11);
+ }
+
+ @Test
+ public void testClickOnIconOfSelectedItem_RemovesFromSelection() {
+ mDetailsLookup.initAt(8).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+ mSelection.assertSelected(8, 9, 10, 11);
+
+ mDetailsLookup.initAt(9);
+ mInputDelegate.onSingleTapUp(CLICK);
+ mSelection.assertSelected(8, 10, 11);
+ }
+
+ @Test
+ public void testRightClickDown_StartsContextMenu() {
+ mInputDelegate.onDown(SECONDARY_CLICK);
+
+ mMouseCallbacks.assertLastEvent(SECONDARY_CLICK);
+ }
+
+ @Test
+ public void testAltClickDown_StartsContextMenu() {
+ mInputDelegate.onDown(ALT_CLICK);
+
+ mMouseCallbacks.assertLastEvent(ALT_CLICK);
+ }
+
+ @Test
+ public void testScroll_shouldTrap() {
+ mDetailsLookup.initAt(0);
+ assertTrue(mInputDelegate.onScroll(
+ null,
+ mEvent.action(MotionEvent.ACTION_MOVE).primary().build(),
+ -1,
+ -1));
+ }
+
+ @Test
+ public void testScroll_NoTrapForTwoFinger() {
+ mDetailsLookup.initAt(0);
+ assertFalse(mInputDelegate.onScroll(
+ null,
+ mEvent.action(MotionEvent.ACTION_MOVE).build(),
+ -1,
+ -1));
+ }
+
+ @Test
+ public void testUnconfirmedCtrlClick_AddsToExistingSelection() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(CTRL_CLICK);
+
+ mSelection.assertSelection(7, 11);
+ }
+
+ @Test
+ public void testUnconfirmedShiftClick_ExtendsSelection() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertSelection(7, 8, 9, 10, 11);
+ }
+
+ @Test
+ public void testConfirmedShiftClick_ExtendsSelectionFromFocus() {
+ TestItemDetails item = mDetailsLookup.initAt(7);
+ mFocusCallbacks.focusItem(item);
+
+ // There should be no selected item at this point, just focus on "7".
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapConfirmed(SHIFT_CLICK);
+ mSelection.assertSelection(7, 8, 9, 10, 11);
+ }
+
+ @Test
+ public void testUnconfirmedShiftClick_RotatesAroundOrigin() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+ mSelection.assertSelection(7, 8, 9, 10, 11);
+
+ mDetailsLookup.initAt(5);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertSelection(5, 6, 7);
+ mSelection.assertNotSelected(8, 9, 10, 11);
+ }
+
+ @Test
+ public void testUnconfirmedShiftCtrlClick_Combination() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+ mSelection.assertSelection(7, 8, 9, 10, 11);
+
+ mDetailsLookup.initAt(5);
+ mInputDelegate.onSingleTapUp(CTRL_CLICK);
+
+ mSelection.assertSelection(5, 7, 8, 9, 10, 11);
+ }
+
+ @Test
+ public void testUnconfirmedShiftCtrlClick_ShiftTakesPriority() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(mEvent.ctrl().shift().build());
+
+ mSelection.assertSelection(7, 8, 9, 10, 11);
+ }
+
+ // TODO: Add testSpaceBar_Previews, but we need to set a system property
+ // to have a deterministic state.
+
+ @Test
+ public void testDoubleClick_Opens() {
+ TestItemDetails doc = mDetailsLookup.initAt(11);
+ mInputDelegate.onDoubleTap(CLICK);
+
+ mActivationCallbacks.assertActivated(doc);
+ }
+
+ @Test
+ public void testMiddleClick_DoesNothing() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(TERTIARY_CLICK);
+
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testClickOff_ClearsSelection() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+ mInputDelegate.onSingleTapUp(CLICK);
+
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testClick_Focuses() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(false);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mFocusCallbacks.assertHasFocus(true);
+ mFocusCallbacks.assertFocused("11");
+ }
+
+ @Test
+ public void testClickOff_ClearsFocus() {
+ mDetailsLookup.initAt(11).setInItemSelectRegion(false);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+ mFocusCallbacks.assertHasFocus(true);
+
+ mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+ mInputDelegate.onSingleTapUp(CLICK);
+ mFocusCallbacks.assertHasFocus(false);
+ }
+
+ @Test
+ public void testClickOffSelection_RemovesSelectionAndFocuses() {
+ mDetailsLookup.initAt(1).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(5);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertSelection(1, 2, 3, 4, 5);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(CLICK);
+
+ mFocusCallbacks.assertFocused("11");
+ mSelection.assertNoSelection();
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java
new file mode 100644
index 0000000..3295ea0
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SECONDARY_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SHIFT_CLICK;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetails;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestMouseCallbacks;
+
+/**
+ * MouseInputDelegate / SelectHelper integration test covering the shared
+ * responsibility of range selection.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class MouseInputHandler_RangeTest {
+
+ private static final List<String> ITEMS = TestData.createStringData(100);
+
+ private MouseInputHandler<String> mInputDelegate;
+ private SelectionProbe mSelection;
+ private TestFocusCallbacks<String> mFocusCallbacks;
+ private TestItemDetailsLookup mDetailsLookup;
+
+ @Before
+ public void setUp() {
+ SelectionHelper<String> selectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+ TestMouseCallbacks mouseCallbacks = new TestMouseCallbacks();
+ TestActivationCallbacks<String> activationCallbacks = new TestActivationCallbacks<>();
+
+ mDetailsLookup = new TestItemDetailsLookup();
+ mSelection = new SelectionProbe(selectionMgr);
+ mFocusCallbacks = new TestFocusCallbacks<>();
+
+ mInputDelegate = new MouseInputHandler<>(
+ selectionMgr,
+ new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+ mDetailsLookup,
+ mouseCallbacks,
+ activationCallbacks,
+ mFocusCallbacks);
+ }
+
+ @Test
+ public void testExtendRange() {
+ // uni-click just focuses.
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(7, 11);
+ }
+
+ @Test
+ public void testExtendRangeContinues() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mDetailsLookup.initAt(21);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(7, 21);
+ }
+
+ @Test
+ public void testMultipleContiguousRanges() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ // click without shift sets a new range start point.
+ TestItemDetails item = mDetailsLookup.initAt(20);
+ mInputDelegate.onSingleTapUp(CLICK);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mFocusCallbacks.focusItem(item);
+
+ mDetailsLookup.initAt(25);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+ mInputDelegate.onSingleTapConfirmed(SHIFT_CLICK);
+
+ mSelection.assertRangeNotSelected(7, 11);
+ mSelection.assertRangeSelected(20, 25);
+ mSelection.assertSelectionSize(6);
+ }
+
+ @Test
+ public void testReducesSelectionRange() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(17);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mDetailsLookup.initAt(10);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(7, 10);
+ }
+
+ @Test
+ public void testReducesSelectionRange_Reverse() {
+ mDetailsLookup.initAt(17).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(7);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mDetailsLookup.initAt(14);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(14, 17);
+ }
+
+ @Test
+ public void testExtendsRange_Reverse() {
+ mDetailsLookup.initAt(12).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(5);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(5, 12);
+ }
+
+ @Test
+ public void testExtendsRange_ReversesAfterForwardClick() {
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapConfirmed(CLICK);
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mDetailsLookup.initAt(0);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(0, 7);
+ }
+
+ @Test
+ public void testRightClickEstablishesRange() {
+
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onDown(SECONDARY_CLICK);
+ // This next method call simulates the behavior of the system event dispatch code.
+ // UserInputHandler depends on a specific sequence of events for internal
+ // state to remain valid. It's not an awesome arrangement, but it is currently
+ // necessary.
+ //
+ // See: UserInputHandler.MouseDelegate#mHandledOnDown;
+ mInputDelegate.onSingleTapUp(SECONDARY_CLICK);
+
+ mDetailsLookup.initAt(11);
+ // Now we can send a subsequent event that should extend selection.
+ mInputDelegate.onDown(SHIFT_CLICK);
+ mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+ mSelection.assertRangeSelection(7, 11);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java
new file mode 100644
index 0000000..0daeb41
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+
+import static androidx.recyclerview.selection.Range.TYPE_PRIMARY;
+import static androidx.recyclerview.selection.Range.TYPE_PROVISIONAL;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Stack;
+
+import androidx.recyclerview.selection.testing.TestData;
+
+/**
+ * MouseInputDelegate / SelectHelper integration test covering the shared
+ * responsibility of range selection.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class RangeTest {
+
+ private static final List<String> ITEMS = TestData.createStringData(100);
+
+ private RangeSpy mSpy;
+ private Stack<Capture> mOperations;
+ private Range mRange;
+
+ @Before
+ public void setUp() {
+ mOperations = new Stack<>();
+ mSpy = new RangeSpy(mOperations);
+ }
+
+ @Test
+ public void testEstablishRange() {
+ mRange = new Range(0, mSpy);
+ mRange.extendRange(5, TYPE_PRIMARY);
+
+ // Origin is expected to have already been selected.
+ mOperations.pop().assertChanged(1, 5, true);
+ }
+
+ @Test
+ public void testExpandRange() {
+ mRange = new Range(0, mSpy);
+ mRange.extendRange(5, TYPE_PRIMARY);
+ mRange.extendRange(10, TYPE_PRIMARY);
+
+ mOperations.pop().assertChanged(6, 10, true);
+ }
+
+ @Test
+ public void testContractRange() {
+ mRange = new Range(0, mSpy);
+ mRange.extendRange(10, TYPE_PRIMARY);
+ mRange.extendRange(5, TYPE_PRIMARY);
+ mOperations.pop().assertChanged(6, 10, false);
+ }
+
+
+ @Test
+ public void testFlipRange_InitiallyDescending() {
+ mRange = new Range(10, mSpy);
+ mRange.extendRange(20, TYPE_PRIMARY);
+ mRange.extendRange(5, TYPE_PRIMARY);
+
+ // When a revision results in a flip two changes
+ // are sent to the callback. 1 to unselect the old items
+ // and one to select the new items.
+ mOperations.pop().assertChanged(5, 9, true);
+ // note that range never modifies the anchor.
+ mOperations.pop().assertChanged(11, 20, false);
+ }
+
+ @Test
+ public void testFlipRange_InitiallyAscending() {
+ mRange = new Range(10, mSpy);
+ mRange.extendRange(5, TYPE_PRIMARY);
+ mRange.extendRange(20, TYPE_PRIMARY);
+
+ // When a revision results in a flip two changes
+ // are sent to the callback. 1 to unselect the old items
+ // and one to select the new items.
+ mOperations.pop().assertChanged(11, 20, true);
+ // note that range never modifies the anchor.
+ mOperations.pop().assertChanged(5, 9, false);
+ }
+
+ // NOTE: The operation type is conveyed among methods, then
+ // returned to the caller. It's more of something we coury
+ // for the caller. So we won't verify courying the value
+ // with all behaviors. Just this once.
+ @Test
+ public void testCouriesRangeType() {
+ mRange = new Range(0, mSpy);
+
+ mRange.extendRange(5, TYPE_PRIMARY);
+ mOperations.pop().assertType(TYPE_PRIMARY);
+
+ mRange.extendRange(10, TYPE_PROVISIONAL);
+ mOperations.pop().assertType(TYPE_PROVISIONAL);
+ }
+
+ private static class Capture {
+
+ private int mBegin;
+ private int mEnd;
+ private boolean mSelected;
+ private int mType;
+
+ private Capture(int begin, int end, boolean selected, int type) {
+ mBegin = begin;
+ mEnd = end;
+ mSelected = selected;
+ mType = type;
+ }
+
+ private void assertType(int expected) {
+ assertEquals(expected, mType);
+ }
+
+ private void assertChanged(int begin, int end, boolean selected) {
+ assertEquals(begin, mBegin);
+ assertEquals(end, mEnd);
+ assertEquals(selected, mSelected);
+ }
+ }
+
+ private static final class RangeSpy extends Range.Callbacks {
+
+ private final Stack<Capture> mOperations;
+
+ RangeSpy(Stack<Capture> operations) {
+ mOperations = operations;
+ }
+
+ @Override
+ void updateForRange(int begin, int end, boolean selected, int type) {
+ mOperations.push(new Capture(begin, end, selected, type));
+ }
+
+ Capture popOp() {
+ return mOperations.pop();
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java
new file mode 100644
index 0000000..5535935
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.recyclerview.selection.testing.Bundles;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.TestData;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class SelectionStorage_LongsTest {
+
+ private SelectionHelper<Long> mSelectionHelper;
+ private SelectionStorage<Long> mSelectionStorage;
+ private Bundle mBundle;
+
+ @Before
+ public void setUp() {
+ mSelectionHelper = SelectionHelpers.createTestInstance(TestData.createLongData(100));
+ mSelectionStorage = new SelectionStorage<>(
+ SelectionStorage.TYPE_LONG, mSelectionHelper);
+ mBundle = new Bundle();
+ }
+
+ @Test
+ public void testWritesSelectionToBundle() {
+ mSelectionHelper.select(3L);
+ mSelectionStorage.onSaveInstanceState(mBundle);
+ Bundle out = Bundles.forceParceling(mBundle);
+ assertTrue(out.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_TYPE));
+ assertTrue(out.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_ENTRIES));
+ }
+
+ @Test
+ public void testRestoresFromSelectionInBundle() {
+ mSelectionHelper.select(3L);
+ mSelectionHelper.select(13L);
+ mSelectionHelper.select(33L);
+
+ MutableSelection orig = new MutableSelection();
+ mSelectionHelper.copySelection(orig);
+ mSelectionStorage.onSaveInstanceState(mBundle);
+ Bundle out = Bundles.forceParceling(mBundle);
+
+ mSelectionHelper.clearSelection();
+ mSelectionStorage.onRestoreInstanceState(out);
+ MutableSelection restored = new MutableSelection();
+ mSelectionHelper.copySelection(restored);
+ assertEquals(orig, restored);
+ }
+
+ @Test
+ public void testIgnoresNullBundle() {
+ mSelectionStorage.onRestoreInstanceState(null);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java
new file mode 100644
index 0000000..9442be9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.recyclerview.selection.testing.Bundles;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.TestData;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class SelectionStorage_StringsTest {
+
+ private SelectionHelper<String> mSelectionHelper;
+ private SelectionStorage<String> mSelectionStorage;
+ private Bundle mBundle;
+
+ @Before
+ public void setUp() {
+ mSelectionHelper = SelectionHelpers.createTestInstance(TestData.createStringData(100));
+ mSelectionStorage = new SelectionStorage<>(
+ SelectionStorage.TYPE_STRING, mSelectionHelper);
+ mBundle = new Bundle();
+ }
+
+ @Test
+ public void testWritesSelectionToBundle() {
+ mSelectionHelper.select("3");
+ mSelectionStorage.onSaveInstanceState(mBundle);
+ Bundle out = Bundles.forceParceling(mBundle);
+
+ assertTrue(mBundle.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_ENTRIES));
+ }
+
+ @Test
+ public void testRestoresFromSelectionInBundle() {
+ mSelectionHelper.select("3");
+ mSelectionHelper.select("13");
+ mSelectionHelper.select("33");
+
+ MutableSelection orig = new MutableSelection();
+ mSelectionHelper.copySelection(orig);
+ mSelectionStorage.onSaveInstanceState(mBundle);
+ Bundle out = Bundles.forceParceling(mBundle);
+
+ mSelectionHelper.clearSelection();
+
+ mSelectionStorage.onRestoreInstanceState(mBundle);
+ MutableSelection restored = new MutableSelection();
+ mSelectionHelper.copySelection(restored);
+ assertEquals(orig, restored);
+ }
+
+ @Test
+ public void testIgnoresNullBundle() {
+ mSelectionStorage.onRestoreInstanceState(null);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java
new file mode 100644
index 0000000..64bf01d
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SelectionTest {
+
+ private final String[] mIds = new String[] {
+ "foo",
+ "43",
+ "auth|id=@53di*/f3#d"
+ };
+
+ private Selection mSelection;
+
+ @Before
+ public void setUp() throws Exception {
+ mSelection = new Selection();
+ mSelection.add(mIds[0]);
+ mSelection.add(mIds[1]);
+ mSelection.add(mIds[2]);
+ }
+
+ @Test
+ public void testAdd() {
+ // We added in setUp.
+ assertEquals(3, mSelection.size());
+ assertContains(mIds[0]);
+ assertContains(mIds[1]);
+ assertContains(mIds[2]);
+ }
+
+ @Test
+ public void testRemove() {
+ mSelection.remove(mIds[0]);
+ mSelection.remove(mIds[2]);
+ assertEquals(1, mSelection.size());
+ assertContains(mIds[1]);
+ }
+
+ @Test
+ public void testClear() {
+ mSelection.clear();
+ assertEquals(0, mSelection.size());
+ }
+
+ @Test
+ public void testIsEmpty() {
+ assertTrue(new Selection().isEmpty());
+ mSelection.clear();
+ assertTrue(mSelection.isEmpty());
+ }
+
+ @Test
+ public void testSize() {
+ Selection other = new Selection();
+ for (int i = 0; i < mSelection.size(); i++) {
+ other.add(mIds[i]);
+ }
+ assertEquals(mSelection.size(), other.size());
+ }
+
+ @Test
+ public void testEqualsSelf() {
+ assertEquals(mSelection, mSelection);
+ }
+
+ @Test
+ public void testEqualsOther() {
+ Selection other = new Selection();
+ other.add(mIds[0]);
+ other.add(mIds[1]);
+ other.add(mIds[2]);
+ assertEquals(mSelection, other);
+ assertEquals(mSelection.hashCode(), other.hashCode());
+ }
+
+ @Test
+ public void testEqualsCopy() {
+ Selection other = new Selection();
+ other.copyFrom(mSelection);
+ assertEquals(mSelection, other);
+ assertEquals(mSelection.hashCode(), other.hashCode());
+ }
+
+ @Test
+ public void testNotEquals() {
+ Selection other = new Selection();
+ other.add("foobar");
+ assertFalse(mSelection.equals(other));
+ }
+
+ private void assertContains(String id) {
+ String err = String.format("Selection %s does not contain %s", mSelection, id);
+ assertTrue(err, mSelection.contains(id));
+ }
+
+ public static <E> Set<E> newSet(E... elements) {
+ HashSet<E> set = new HashSet<>(elements.length);
+ Collections.addAll(set, elements);
+ return set;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java
new file mode 100644
index 0000000..476684a
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Touch.TAP;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestRunnable;
+import androidx.recyclerview.selection.testing.TestSelectionPredicate;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TouchInputHandlerTest {
+
+ private static final List<String> ITEMS = TestData.createStringData(100);
+
+ private TouchInputHandler mInputDelegate;
+ private SelectionHelper mSelectionMgr;
+ private TestSelectionPredicate mSelectionPredicate;
+ private TestRunnable mGestureStarted;
+ private TestRunnable mHapticPerformer;
+ private TestTouchCallbacks mMouseCallbacks;
+ private TestActivationCallbacks mActivationCallbacks;
+ private TestFocusCallbacks mFocusCallbacks;
+ private TestItemDetailsLookup mDetailsLookup;
+ private SelectionProbe mSelection;
+
+ @Before
+ public void setUp() {
+ mSelectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+ mDetailsLookup = new TestItemDetailsLookup();
+ mSelectionPredicate = new TestSelectionPredicate();
+ mSelection = new SelectionProbe(mSelectionMgr);
+ mGestureStarted = new TestRunnable();
+ mHapticPerformer = new TestRunnable();
+ mMouseCallbacks = new TestTouchCallbacks();
+ mActivationCallbacks = new TestActivationCallbacks();
+ mFocusCallbacks = new TestFocusCallbacks();
+
+ mInputDelegate = new TouchInputHandler(
+ mSelectionMgr,
+ new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+ mDetailsLookup,
+ mSelectionPredicate,
+ mGestureStarted,
+ mMouseCallbacks,
+ mActivationCallbacks,
+ mFocusCallbacks,
+ mHapticPerformer);
+ }
+
+ @Test
+ public void testTap_ActivatesWhenNoExistingSelection() {
+ ItemDetails doc = mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mActivationCallbacks.assertActivated(doc);
+ }
+
+ @Test
+ public void testScroll_shouldNotBeTrapped() {
+ assertFalse(mInputDelegate.onScroll(null, TAP, -1, -1));
+ }
+
+ @Test
+ public void testLongPress_SelectsItem() {
+ mSelectionPredicate.setReturnValue(true);
+
+ mDetailsLookup.initAt(7);
+ mInputDelegate.onLongPress(TAP);
+
+ mSelection.assertSelection(7);
+ }
+
+ @Test
+ public void testLongPress_StartsGestureSelection() {
+ mSelectionPredicate.setReturnValue(true);
+
+ mDetailsLookup.initAt(7);
+ mInputDelegate.onLongPress(TAP);
+ mGestureStarted.assertRan();
+ }
+
+ @Test
+ public void testSelectHotspot_StartsSelectionMode() {
+ mSelectionPredicate.setReturnValue(true);
+
+ mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mSelection.assertSelection(7);
+ }
+
+ @Test
+ public void testSelectionHotspot_UnselectsSelectedItem() {
+ mSelectionMgr.select("11");
+
+ mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testStartsSelection_PerformsHapticFeedback() {
+ mSelectionPredicate.setReturnValue(true);
+
+ mDetailsLookup.initAt(7);
+ mInputDelegate.onLongPress(TAP);
+
+ mHapticPerformer.assertRan();
+ }
+
+ @Test
+ public void testLongPress_AddsToSelection() {
+ mSelectionPredicate.setReturnValue(true);
+
+ mDetailsLookup.initAt(7);
+ mInputDelegate.onLongPress(TAP);
+
+ mDetailsLookup.initAt(99);
+ mInputDelegate.onLongPress(TAP);
+
+ mDetailsLookup.initAt(13);
+ mInputDelegate.onLongPress(TAP);
+
+ mSelection.assertSelection(7, 13, 99);
+ }
+
+ @Test
+ public void testTap_UnselectsSelectedItem() {
+ mSelectionMgr.select("11");
+
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mSelection.assertNoSelection();
+ }
+
+ @Test
+ public void testTapOff_ClearsSelection() {
+ mSelectionMgr.select("7");
+ mDetailsLookup.initAt(7);
+
+ mInputDelegate.onLongPress(TAP);
+
+ mSelectionMgr.select("11");
+ mDetailsLookup.initAt(11);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mDetailsLookup.initAt(RecyclerView.NO_POSITION).setInItemSelectRegion(false);
+ mInputDelegate.onSingleTapUp(TAP);
+
+ mSelection.assertNoSelection();
+ }
+
+ private static final class TestTouchCallbacks extends TouchCallbacks {
+ @Override
+ public boolean onDragInitiated(MotionEvent e) {
+ return false;
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java
new file mode 100644
index 0000000..66cb721
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.recyclerview.selection.ViewAutoScroller.ScrollHost;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class ViewAutoScrollerTest {
+
+ private static final float SCROLL_THRESHOLD_RATIO = 0.125f;
+ private static final int VIEW_HEIGHT = 100;
+ private static final int TOP_Y_POINT = (int) (VIEW_HEIGHT * SCROLL_THRESHOLD_RATIO) - 1;
+ private static final int BOTTOM_Y_POINT =
+ VIEW_HEIGHT - (int) (VIEW_HEIGHT * SCROLL_THRESHOLD_RATIO) + 1;
+
+ private ViewAutoScroller mScroller;
+ private TestHost mHost;
+
+ @Before
+ public void setUp() {
+ mHost = new TestHost();
+ mScroller = new ViewAutoScroller(mHost, SCROLL_THRESHOLD_RATIO);
+ }
+
+ @Test
+ public void testNoScrollWhenOutOfScrollZone() {
+ mScroller.scroll(new Point(0, VIEW_HEIGHT / 2));
+ mHost.run();
+ mHost.assertNotScrolled();
+ }
+
+// @Test
+// public void testNoScrollWhenDisabled() {
+// mScroller.reset();
+// mScroller.scroll(mEvent.location(0, TOP_Y_POINT).build());
+// mHost.assertNotScrolled();
+// }
+
+ @Test
+ public void testMotionThreshold() {
+ mScroller.scroll(new Point(0, TOP_Y_POINT));
+ mHost.run();
+
+ mScroller.scroll(new Point(0, TOP_Y_POINT - 1));
+ mHost.run();
+
+ mHost.assertNotScrolled();
+ }
+
+ @Test
+ public void testMotionThreshold_Resets() {
+ int expectedScrollDistance = mScroller.computeScrollDistance(-21);
+ mScroller.scroll(new Point(0, TOP_Y_POINT));
+ mHost.run();
+ // We need enough y motion to overcome motion threshold
+ mScroller.scroll(new Point(0, TOP_Y_POINT - 20));
+ mHost.run();
+
+ mHost.reset();
+ // After resetting events should be required to cross the motion threshold
+ // before auto-scrolling again.
+ mScroller.reset();
+
+ mScroller.scroll(new Point(0, TOP_Y_POINT));
+ mHost.run();
+
+ mHost.assertNotScrolled();
+ }
+
+ @Test
+ public void testAutoScrolls_Top() {
+ int expectedScrollDistance = mScroller.computeScrollDistance(-21);
+ mScroller.scroll(new Point(0, TOP_Y_POINT));
+ mHost.run();
+ // We need enough y motion to overcome motion threshold
+ mScroller.scroll(new Point(0, TOP_Y_POINT - 20));
+ mHost.run();
+
+ mHost.assertScrolledBy(expectedScrollDistance);
+ }
+
+ @Test
+ public void testAutoScrolls_Bottom() {
+ int expectedScrollDistance = mScroller.computeScrollDistance(21);
+ mScroller.scroll(new Point(0, BOTTOM_Y_POINT));
+ mHost.run();
+ // We need enough y motion to overcome motion threshold
+ mScroller.scroll(new Point(0, BOTTOM_Y_POINT + 20));
+ mHost.run();
+
+ mHost.assertScrolledBy(expectedScrollDistance);
+ }
+
+ private final class TestHost extends ScrollHost {
+
+ private @Nullable Integer mScrollDistance;
+ private @Nullable Runnable mRunnable;
+
+ @Override
+ int getViewHeight() {
+ return VIEW_HEIGHT;
+ }
+
+ @Override
+ void scrollBy(int distance) {
+ mScrollDistance = distance;
+ }
+
+ @Override
+ void runAtNextFrame(Runnable r) {
+ mRunnable = r;
+ }
+
+ @Override
+ void removeCallback(Runnable r) {
+ }
+
+ private void reset() {
+ mScrollDistance = null;
+ mRunnable = null;
+ }
+
+ private void run() {
+ mRunnable.run();
+ }
+
+ private void assertNotScrolled() {
+ assertNull(mScrollDistance);
+ }
+
+ private void assertScrolledBy(int expectedDistance) {
+ assertNotNull(mScrollDistance);
+ assertEquals(expectedDistance, mScrollDistance.intValue());
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java
new file mode 100644
index 0000000..6ceba34
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+public final class Bundles {
+
+ private Bundles() {
+ }
+
+ public static Bundle forceParceling(Bundle in) {
+ Parcel parcel = Parcel.obtain();
+ in.writeToParcel(parcel, 0);
+
+ parcel.setDataPosition(0);
+ return parcel.readBundle();
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java
new file mode 100644
index 0000000..9a031a9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.DefaultSelectionHelper;
+import androidx.recyclerview.selection.EventBridge;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionPredicates;
+
+public final class SelectionHelpers {
+
+ private SelectionHelpers() {}
+
+ public static <K> SelectionHelper<K> createTestInstance(List<K> items) {
+ TestAdapter<K> adapter = new TestAdapter<>(items);
+ ItemKeyProvider<K> keyProvider =
+ new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, adapter);
+ SelectionHelper<K> helper = new DefaultSelectionHelper<>(
+ keyProvider,
+ SelectionPredicates.selectAnything());
+
+ EventBridge.install(adapter, helper, keyProvider);
+
+ return helper;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java
new file mode 100644
index 0000000..4501078
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.recyclerview.selection.DefaultSelectionHelper;
+import androidx.recyclerview.selection.Selection;
+import androidx.recyclerview.selection.SelectionHelper;
+
+/**
+ * Helper class for making assertions against the state of a {@link DefaultSelectionHelper} instance
+ * and the consistency of states between {@link DefaultSelectionHelper} and
+ * {@link DefaultSelectionHelper.SelectionObserver}.
+ */
+public final class SelectionProbe {
+
+ private final SelectionHelper<String> mMgr;
+ private final TestSelectionObserver<String> mSelectionListener;
+
+ public SelectionProbe(SelectionHelper<String> mgr) {
+ mMgr = mgr;
+ mSelectionListener = new TestSelectionObserver<String>();
+ mMgr.addObserver(mSelectionListener);
+ }
+
+ public SelectionProbe(
+ SelectionHelper<String> mgr, TestSelectionObserver<String> selectionListener) {
+ mMgr = mgr;
+ mSelectionListener = selectionListener;
+ }
+
+ public void assertRangeSelected(int begin, int end) {
+ for (int i = begin; i <= end; i++) {
+ assertSelected(i);
+ }
+ }
+
+ public void assertRangeNotSelected(int begin, int end) {
+ for (int i = begin; i <= end; i++) {
+ assertNotSelected(i);
+ }
+ }
+
+ public void assertRangeSelection(int begin, int end) {
+ assertSelectionSize(end - begin + 1);
+ assertRangeSelected(begin, end);
+ }
+
+ public void assertSelectionSize(int expected) {
+ Selection selection = mMgr.getSelection();
+ assertEquals(selection.toString(), expected, selection.size());
+
+ mSelectionListener.assertSelectionSize(expected);
+ }
+
+ public void assertNoSelection() {
+ assertSelectionSize(0);
+
+ mSelectionListener.assertNoSelection();
+ }
+
+ public void assertSelection(int... ids) {
+ assertSelected(ids);
+ assertEquals(ids.length, mMgr.getSelection().size());
+
+ mSelectionListener.assertSelectionSize(ids.length);
+ }
+
+ public void assertSelected(int... ids) {
+ Selection<String> sel = mMgr.getSelection();
+ for (int id : ids) {
+ String sid = String.valueOf(id);
+ assertTrue(sid + " is not in selection " + sel, sel.contains(sid));
+
+ mSelectionListener.assertSelected(sid);
+ }
+ }
+
+ public void assertNotSelected(int... ids) {
+ Selection<String> sel = mMgr.getSelection();
+ for (int id : ids) {
+ String sid = String.valueOf(id);
+ assertFalse(sid + " is in selection " + sel, sel.contains(sid));
+
+ mSelectionListener.assertNotSelected(sid);
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java
new file mode 100644
index 0000000..b106a92
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ActivationCallbacks;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestActivationCallbacks<K> extends ActivationCallbacks<K> {
+
+ private ItemDetails<K> mActivated;
+
+ @Override
+ public boolean onItemActivated(ItemDetails<K> item, MotionEvent e) {
+ mActivated = item;
+ return true;
+ }
+
+ public void assertActivated(ItemDetails<K> expected) {
+ assertEquals(expected, mActivated);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java
new file mode 100644
index 0000000..d9e952e
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertTrue;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.recyclerview.selection.SelectionHelper;
+
+public class TestAdapter<K> extends Adapter<TestHolder> {
+
+ private final List<K> mItems = new ArrayList<>();
+ private final List<Integer> mNotifiedOfSelection = new ArrayList<>();
+ private final AdapterDataObserver mAdapterObserver;
+
+ public TestAdapter() {
+ this(Collections.EMPTY_LIST);
+ }
+
+ public TestAdapter(List<K> items) {
+ mItems.addAll(items);
+ mAdapterObserver = new RecyclerView.AdapterDataObserver() {
+
+ @Override
+ public void onChanged() {
+ }
+
+ @Override
+ public void onItemRangeChanged(int startPosition, int itemCount, Object payload) {
+ if (SelectionHelper.SELECTION_CHANGED_MARKER.equals(payload)) {
+ int last = startPosition + itemCount;
+ for (int i = startPosition; i < last; i++) {
+ mNotifiedOfSelection.add(i);
+ }
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ registerAdapterDataObserver(mAdapterObserver);
+ }
+
+ @Override
+ public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new TestHolder(parent);
+ }
+
+ @Override
+ public void onBindViewHolder(TestHolder holder, int position) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ public void updateTestModelIds(List<K> items) {
+ mItems.clear();
+ mItems.addAll(items);
+
+ notifyDataSetChanged();
+ }
+
+ public int getPosition(K key) {
+ return mItems.indexOf(key);
+ }
+
+ public K getSelectionKey(int position) {
+ return mItems.get(position);
+ }
+
+
+ public void resetSelectionNotifications() {
+ mNotifiedOfSelection.clear();
+ }
+
+ public void assertNotifiedOfSelectionChange(int position) {
+ assertTrue(mNotifiedOfSelection.contains(position));
+ }
+
+ public static List<String> createItemList(int num) {
+ List<String> items = new ArrayList<>(num);
+ for (int i = 0; i < num; ++i) {
+ items.add(Integer.toString(i));
+ }
+ return items;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java
new file mode 100644
index 0000000..4232205
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.graphics.Point;
+
+import androidx.recyclerview.selection.AutoScroller;
+
+public class TestAutoScroller extends AutoScroller {
+
+ @Override
+ protected void reset() {
+ }
+
+ @Override
+ protected void scroll(Point location) {
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java
new file mode 100644
index 0000000..ca21b9c
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.BandPredicate;
+
+public class TestBandPredicate extends BandPredicate {
+
+ private boolean mCanInitiate = true;
+
+ public void setCanInitiate(boolean canInitiate) {
+ mCanInitiate = canInitiate;
+ }
+
+ @Override
+ public boolean canInitiate(MotionEvent e) {
+ return mCanInitiate;
+ }
+
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java
new file mode 100644
index 0000000..14e27aa
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestData {
+
+ public static List<String> createStringData(int num) {
+ List<String> items = new ArrayList<>(num);
+ for (int i = 0; i < num; ++i) {
+ items.add(Integer.toString(i));
+ }
+ return items;
+ }
+
+ public static List<Long> createLongData(int num) {
+ List<Long> items = new ArrayList<>(num);
+ for (int i = 0; i < num; ++i) {
+ items.add(new Long(i));
+ }
+ return items;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java
new file mode 100644
index 0000000..fd4bea4
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.graphics.Point;
+import android.support.annotation.IntDef;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Handy-dandy wrapper class to facilitate the creation of MotionEvents.
+ */
+public final class TestEvents {
+
+ /**
+ * Common mouse event types...for your convenience.
+ */
+ public static final class Mouse {
+ public static final MotionEvent CLICK =
+ TestEvents.builder().mouse().primary().build();
+ public static final MotionEvent CTRL_CLICK =
+ TestEvents.builder().mouse().primary().ctrl().build();
+ public static final MotionEvent ALT_CLICK =
+ TestEvents.builder().mouse().primary().alt().build();
+ public static final MotionEvent SHIFT_CLICK =
+ TestEvents.builder().mouse().primary().shift().build();
+ public static final MotionEvent SECONDARY_CLICK =
+ TestEvents.builder().mouse().secondary().build();
+ public static final MotionEvent TERTIARY_CLICK =
+ TestEvents.builder().mouse().tertiary().build();
+ }
+
+ /**
+ * Common touch event types...for your convenience.
+ */
+ public static final class Touch {
+ public static final MotionEvent TAP =
+ TestEvents.builder().touch().build();
+ }
+
+ static final int ACTION_UNSET = -1;
+
+ // Add other actions from MotionEvent.ACTION_ as needed.
+ @IntDef(flag = true, value = {
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_MOVE,
+ MotionEvent.ACTION_UP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Action {}
+
+ // Add other types from MotionEvent.TOOL_TYPE_ as needed.
+ @IntDef(flag = true, value = {
+ MotionEvent.TOOL_TYPE_FINGER,
+ MotionEvent.TOOL_TYPE_MOUSE,
+ MotionEvent.TOOL_TYPE_STYLUS,
+ MotionEvent.TOOL_TYPE_UNKNOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ToolType {}
+
+ @IntDef(flag = true, value = {
+ MotionEvent.BUTTON_PRIMARY,
+ MotionEvent.BUTTON_SECONDARY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Button {}
+
+ @IntDef(flag = true, value = {
+ KeyEvent.META_SHIFT_ON,
+ KeyEvent.META_CTRL_ON
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Key {}
+
+ private static final class State {
+ private @Action int mAction = ACTION_UNSET;
+ private @ToolType int mToolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ private int mPointerCount = 1;
+ private Set<Integer> mButtons = new HashSet<>();
+ private Set<Integer> mKeys = new HashSet<>();
+ private Point mLocation = new Point(0, 0);
+ private Point mRawLocation = new Point(0, 0);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Test event builder with convenience methods for common event attrs.
+ */
+ public static final class Builder {
+
+ private State mState = new State();
+
+ /**
+ * @param action Any action specified in {@link MotionEvent}.
+ * @return
+ */
+ public Builder action(int action) {
+ mState.mAction = action;
+ return this;
+ }
+
+ public Builder type(@ToolType int type) {
+ mState.mToolType = type;
+ return this;
+ }
+
+ public Builder location(int x, int y) {
+ mState.mLocation = new Point(x, y);
+ return this;
+ }
+
+ public Builder rawLocation(int x, int y) {
+ mState.mRawLocation = new Point(x, y);
+ return this;
+ }
+
+ public Builder pointerCount(int count) {
+ mState.mPointerCount = count;
+ return this;
+ }
+
+ /**
+ * Adds one or more button press attributes.
+ */
+ public Builder pressButton(@Button int... buttons) {
+ for (int button : buttons) {
+ mState.mButtons.add(button);
+ }
+ return this;
+ }
+
+ /**
+ * Removes one or more button press attributes.
+ */
+ public Builder releaseButton(@Button int... buttons) {
+ for (int button : buttons) {
+ mState.mButtons.remove(button);
+ }
+ return this;
+ }
+
+ /**
+ * Adds one or more key press attributes.
+ */
+ public Builder pressKey(@Key int... keys) {
+ for (int key : keys) {
+ mState.mKeys.add(key);
+ }
+ return this;
+ }
+
+ /**
+ * Removes one or more key press attributes.
+ */
+ public Builder releaseKey(@Button int... keys) {
+ for (int key : keys) {
+ mState.mKeys.remove(key);
+ }
+ return this;
+ }
+
+ public Builder touch() {
+ type(MotionEvent.TOOL_TYPE_FINGER);
+ return this;
+ }
+
+ public Builder mouse() {
+ type(MotionEvent.TOOL_TYPE_MOUSE);
+ return this;
+ }
+
+ public Builder shift() {
+ pressKey(KeyEvent.META_SHIFT_ON);
+ return this;
+ }
+
+ public Builder unshift() {
+ releaseKey(KeyEvent.META_SHIFT_ON);
+ return this;
+ }
+
+ public Builder ctrl() {
+ pressKey(KeyEvent.META_CTRL_ON);
+ return this;
+ }
+
+ public Builder alt() {
+ pressKey(KeyEvent.META_ALT_ON);
+ return this;
+ }
+
+ public Builder primary() {
+ pressButton(MotionEvent.BUTTON_PRIMARY);
+ releaseButton(MotionEvent.BUTTON_SECONDARY);
+ releaseButton(MotionEvent.BUTTON_TERTIARY);
+ return this;
+ }
+
+ public Builder secondary() {
+ pressButton(MotionEvent.BUTTON_SECONDARY);
+ releaseButton(MotionEvent.BUTTON_PRIMARY);
+ releaseButton(MotionEvent.BUTTON_TERTIARY);
+ return this;
+ }
+
+ public Builder tertiary() {
+ pressButton(MotionEvent.BUTTON_TERTIARY);
+ releaseButton(MotionEvent.BUTTON_PRIMARY);
+ releaseButton(MotionEvent.BUTTON_SECONDARY);
+ return this;
+ }
+
+ public MotionEvent build() {
+
+ PointerProperties[] pointers = new PointerProperties[1];
+ pointers[0] = new PointerProperties();
+ pointers[0].id = 0;
+ pointers[0].toolType = mState.mToolType;
+
+ PointerCoords[] coords = new PointerCoords[1];
+ coords[0] = new PointerCoords();
+ coords[0].x = mState.mLocation.x;
+ coords[0].y = mState.mLocation.y;
+
+ int buttons = 0;
+ for (Integer button : mState.mButtons) {
+ buttons |= button;
+ }
+
+ int keys = 0;
+ for (Integer key : mState.mKeys) {
+ keys |= key;
+ }
+
+ return MotionEvent.obtain(
+ 0, // down time
+ 1, // event time
+ mState.mAction,
+ 1, // pointerCount,
+ pointers,
+ coords,
+ keys,
+ buttons,
+ 1.0f, // x precision
+ 1.0f, // y precision
+ 0, // device id
+ 0, // edge flags
+ 0, // int source,
+ 0 // int flags
+ );
+ }
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java
new file mode 100644
index 0000000..46b02ad
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.v7.widget.RecyclerView;
+
+import androidx.recyclerview.selection.FocusCallbacks;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestFocusCallbacks<K> extends FocusCallbacks<K> {
+
+ private K mFocusItemId;
+ private int mFocusPosition;
+
+ @Override
+ public void clearFocus() {
+ mFocusPosition = RecyclerView.NO_POSITION;
+ mFocusItemId = null;
+ }
+
+ @Override
+ public void focusItem(ItemDetails<K> item) {
+ mFocusItemId = item.getSelectionKey();
+ mFocusPosition = item.getPosition();
+ }
+
+ @Override
+ public int getFocusedPosition() {
+ return mFocusPosition;
+ }
+
+ @Override
+ public boolean hasFocusedItem() {
+ return mFocusItemId != null;
+ }
+
+ public void assertHasFocus(boolean focused) {
+ assertEquals(focused, hasFocusedItem());
+ }
+
+ public void assertFocused(String expectedId) {
+ assertEquals(expectedId, mFocusItemId);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java
new file mode 100644
index 0000000..25c6581
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.View;
+
+public class TestHolder extends ViewHolder {
+ public TestHolder(View itemView) {
+ super(itemView);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java
new file mode 100644
index 0000000..f06e32c
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestItemDetails extends ItemDetails<String> {
+
+ private int mPosition;
+ private String mSelectionKey;
+ private boolean mInDragRegion;
+ private boolean mInSelectionHotspot;
+
+ public TestItemDetails() {
+ mPosition = RecyclerView.NO_POSITION;
+ }
+
+ public TestItemDetails(TestItemDetails source) {
+ mPosition = source.mPosition;
+ mSelectionKey = source.mSelectionKey;
+ mInDragRegion = source.mInDragRegion;
+ mInSelectionHotspot = source.mInSelectionHotspot;
+ }
+
+ public void at(int position) {
+ mPosition = position; // this is both "adapter position" and "item position".
+ mSelectionKey = (position == RecyclerView.NO_POSITION)
+ ? null
+ : String.valueOf(position);
+ }
+
+ public void setInItemDragRegion(boolean inHotspot) {
+ mInDragRegion = inHotspot;
+ }
+
+ public void setInItemSelectRegion(boolean over) {
+ mInSelectionHotspot = over;
+ }
+
+ @Override
+ public boolean inDragRegion(MotionEvent event) {
+ return mInDragRegion;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPosition;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof TestItemDetails)) {
+ return false;
+ }
+
+ TestItemDetails other = (TestItemDetails) o;
+ return mPosition == other.mPosition
+ && mSelectionKey == other.mSelectionKey;
+ }
+
+ @Override
+ public int getPosition() {
+ return mPosition;
+ }
+
+ @Override
+ public String getSelectionKey() {
+ return mSelectionKey;
+ }
+
+ @Override
+ public boolean inSelectionHotspot(MotionEvent e) {
+ return mInSelectionHotspot;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java
new file mode 100644
index 0000000..c201575
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import android.view.MotionEvent;
+
+import javax.annotation.Nullable;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Test impl of ItemDetailsLookup.
+ */
+public class TestItemDetailsLookup extends ItemDetailsLookup<String> {
+
+ private @Nullable TestItemDetails mItem;
+
+ @Override
+ public @Nullable ItemDetails<String> getItemDetails(MotionEvent e) {
+ return mItem;
+ }
+
+ /**
+ * Creates/installs/returns a new test document. Subsequent calls to
+ * any EventDocLookup methods will consult the newly created doc.
+ */
+ public TestItemDetails initAt(int position) {
+ TestItemDetails doc = new TestItemDetails();
+ doc.at(position);
+ mItem = doc;
+ return doc;
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java
new file mode 100644
index 0000000..c874ac5
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+
+/**
+ * Provides RecyclerView selection code access to stable ids backed
+ * by TestAdapter.
+ */
+public final class TestItemKeyProvider<K> extends ItemKeyProvider<K> {
+
+ private final TestAdapter<K> mAdapter;
+
+ public TestItemKeyProvider(@Scope int scope, TestAdapter<K> adapter) {
+ super(scope);
+ checkArgument(adapter != null);
+ mAdapter = adapter;
+ }
+
+ @Override
+ public K getKey(int position) {
+ return mAdapter.getSelectionKey(position);
+ }
+
+ @Override
+ public int getPosition(K key) {
+ return mAdapter.getPosition(key);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java
new file mode 100644
index 0000000..321ac7f
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertTrue;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.MouseCallbacks;
+
+public final class TestMouseCallbacks extends MouseCallbacks {
+
+ private MotionEvent mLastContextEvent;
+
+ @Override
+ public boolean onContextClick(MotionEvent e) {
+ mLastContextEvent = e;
+ return false;
+ }
+
+ public void assertLastEvent(MotionEvent expected) {
+ // sadly, MotionEvent doesn't implement equals, so we compare references.
+ assertTrue(expected == mLastContextEvent);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java
new file mode 100644
index 0000000..7a1d45c
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static junit.framework.Assert.assertTrue;
+
+public final class TestRunnable implements Runnable {
+
+ private boolean mRan;
+
+ @Override
+ public void run() {
+ mRan = true;
+ }
+
+ public void assertRan() {
+ assertTrue(mRan);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java
new file mode 100644
index 0000000..3185d78
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+
+public class TestSelectionObserver<K> extends SelectionObserver<K> {
+
+ private final Set<K> mSelected = new HashSet<>();
+ private boolean mSelectionChanged = false;
+ private boolean mSelectionReset = false;
+ private boolean mSelectionRestored = false;
+
+ public void reset() {
+ mSelected.clear();
+ mSelectionChanged = false;
+ mSelectionReset = false;
+ }
+
+ @Override
+ public void onItemStateChanged(K key, boolean selected) {
+ if (selected) {
+ assertNotSelected(key);
+ mSelected.add(key);
+ } else {
+ assertSelected(key);
+ mSelected.remove(key);
+ }
+ }
+
+ @Override
+ public void onSelectionReset() {
+ mSelectionReset = true;
+ mSelected.clear();
+ }
+
+ @Override
+ public void onSelectionChanged() {
+ mSelectionChanged = true;
+ }
+
+ @Override
+ public void onSelectionRestored() {
+ mSelectionRestored = true;
+ }
+
+ void assertNoSelection() {
+ assertTrue(mSelected.isEmpty());
+ }
+
+ void assertSelectionSize(int expected) {
+ assertEquals(expected, mSelected.size());
+ }
+
+ void assertSelected(K key) {
+ assertTrue(key + " is not selected.", mSelected.contains(key));
+ }
+
+ void assertNotSelected(K key) {
+ assertFalse(key + " is already selected", mSelected.contains(key));
+ }
+
+ public void assertSelectionChanged() {
+ assertTrue(mSelectionChanged);
+ }
+
+ public void assertSelectionUnchanged() {
+ assertFalse(mSelectionChanged);
+ }
+
+ public void assertSelectionReset() {
+ assertTrue(mSelectionReset);
+ }
+
+ public void assertSelectionRestored() {
+ assertTrue(mSelectionRestored);
+ }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java
new file mode 100644
index 0000000..4baeb2b
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection.testing;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+public final class TestSelectionPredicate<K> extends SelectionPredicate<K> {
+
+ private final boolean mMultiSelect;
+
+ private boolean mValue;
+
+ public TestSelectionPredicate(boolean multiSelect) {
+ mMultiSelect = multiSelect;
+ }
+
+ public TestSelectionPredicate() {
+ this(true);
+ }
+
+ public void setReturnValue(boolean value) {
+ mValue = value;
+ }
+
+ @Override
+ public boolean canSetStateForKey(K key, boolean nextState) {
+ return mValue;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ return mValue;
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return mMultiSelect;
+ }
+}
diff --git a/room/gradle/wrapper/gradle-wrapper.properties b/room/gradle/wrapper/gradle-wrapper.properties
index 2f8bf03..383477d 100644
--- a/room/gradle/wrapper/gradle-wrapper.properties
+++ b/room/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/samples/Support7Demos/build.gradle b/samples/Support7Demos/build.gradle
index 0b562c8..14d128a 100644
--- a/samples/Support7Demos/build.gradle
+++ b/samples/Support7Demos/build.gradle
@@ -7,6 +7,7 @@
implementation project(':mediarouter-v7')
implementation project(':palette-v7')
implementation project(':recyclerview-v7')
+ implementation project(':recyclerview-selection')
}
android {
diff --git a/samples/Support7Demos/src/main/AndroidManifest.xml b/samples/Support7Demos/src/main/AndroidManifest.xml
index 1604267..d32ea97 100644
--- a/samples/Support7Demos/src/main/AndroidManifest.xml
+++ b/samples/Support7Demos/src/main/AndroidManifest.xml
@@ -563,7 +563,26 @@
<category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
</intent-filter>
</activity>
- </application>
+ <!-- Selection helper demo activity -->
+ <activity android:name=".widget.selection.simple.SimpleSelectionDemoActivity"
+ android:label="@string/simple_selection_demo_activity"
+ android:theme="@style/Theme.AppCompat.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
+ </intent-filter>
+ </activity>
+
+ <!-- Selection helper demo activity -->
+ <activity android:name=".widget.selection.fancy.FancySelectionDemoActivity"
+ android:label="@string/fancy_selection_demo_activity"
+ android:theme="@style/Theme.AppCompat.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
+ </intent-filter>
+ </activity>
+ </application>
</manifest>
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java
new file mode 100644
index 0000000..4b9c158
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.widget.selection.fancy;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+
+class ContentUriKeyProvider extends ItemKeyProvider<Uri> {
+
+ private final Uri[] mUris;
+ private final Map<Uri, Integer> mPositions;
+
+ ContentUriKeyProvider(String authority, String[] values) {
+ // Advise the world we can supply ids/position for entire copus
+ // at any time.
+ super(SCOPE_MAPPED);
+
+ mUris = new Uri[values.length];
+ mPositions = new HashMap<>();
+
+ for (int i = 0; i < values.length; i++) {
+ mUris[i] = new Uri.Builder()
+ .scheme("content")
+ .encodedAuthority(authority)
+ .appendPath(values[i])
+ .build();
+ mPositions.put(mUris[i], i);
+ }
+ }
+
+ @Override
+ public @Nullable Uri getKey(int position) {
+ return mUris[position];
+ }
+
+ @Override
+ public int getPosition(Uri key) {
+ return mPositions.get(key);
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java
new file mode 100644
index 0000000..249d3c2
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.widget.selection.fancy;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Access to details of an item associated with a {@link MotionEvent} instance.
+ */
+final class FancyDetailsLookup extends ItemDetailsLookup<Uri> {
+
+ private final RecyclerView mRecView;
+
+ FancyDetailsLookup(RecyclerView view) {
+ mRecView = view;
+ }
+
+ @Override
+ public ItemDetails<Uri> getItemDetails(MotionEvent e) {
+ @Nullable View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+ if (view != null) {
+ ViewHolder holder = mRecView.getChildViewHolder(view);
+ if (holder instanceof FancyHolder) {
+ return ((FancyHolder) holder).getItemDetails();
+ }
+ }
+ return null;
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java
new file mode 100644
index 0000000..f4b22b3
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.widget.selection.fancy;
+
+import android.graphics.Rect;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+final class FancyHolder extends RecyclerView.ViewHolder {
+
+ private final LinearLayout mContainer;
+ public final TextView mSelector;
+ public final TextView mLabel;
+ private final ItemDetails<Uri> mDetails;
+
+ private @Nullable Uri mKey;
+
+ FancyHolder(LinearLayout layout) {
+ super(layout);
+ mContainer = layout.findViewById(R.id.container);
+ mSelector = layout.findViewById(R.id.selector);
+ mLabel = layout.findViewById(R.id.label);
+ mDetails = new ItemDetails<Uri>() {
+ @Override
+ public int getPosition() {
+ return FancyHolder.this.getAdapterPosition();
+ }
+
+ @Override
+ public Uri getSelectionKey() {
+ return FancyHolder.this.mKey;
+ }
+
+ @Override
+ public boolean inDragRegion(MotionEvent e) {
+ return FancyHolder.this.inDragRegion(e);
+ }
+
+ @Override
+ public boolean inSelectionHotspot(MotionEvent e) {
+ return FancyHolder.this.inSelectRegion(e);
+ }
+ };
+ }
+
+ void update(Uri key, String label, boolean selected) {
+ mKey = key;
+ mLabel.setText(label);
+ setSelected(selected);
+ }
+
+ private void setSelected(boolean selected) {
+ mContainer.setActivated(selected);
+ mSelector.setActivated(selected);
+ }
+
+ boolean inDragRegion(MotionEvent event) {
+ // If itemView is activated = selected, then whole region is interactive
+ if (itemView.isActivated()) {
+ return true;
+ }
+
+ // Do everything in global coordinates - it makes things simpler.
+ int[] coords = new int[2];
+ mSelector.getLocationOnScreen(coords);
+
+ Rect textBounds = new Rect();
+ mLabel.getPaint().getTextBounds(
+ mLabel.getText().toString(), 0, mLabel.getText().length(), textBounds);
+
+ Rect rect = new Rect(
+ coords[0],
+ coords[1],
+ coords[0] + mSelector.getWidth() + textBounds.width(),
+ coords[1] + Math.max(mSelector.getHeight(), textBounds.height()));
+
+ // If the tap occurred inside icon or the text, these are interactive spots.
+ return rect.contains((int) event.getRawX(), (int) event.getRawY());
+ }
+
+ boolean inSelectRegion(MotionEvent e) {
+ Rect iconRect = new Rect();
+ mSelector.getGlobalVisibleRect(iconRect);
+ return iconRect.contains((int) e.getRawX(), (int) e.getRawY());
+ }
+
+ ItemDetails<Uri> getItemDetails() {
+ return mDetails;
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
new file mode 100644
index 0000000..6c389d5
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.widget.selection.fancy;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+import androidx.recyclerview.selection.SelectionHelperBuilder;
+import androidx.recyclerview.selection.SelectionPredicates;
+import androidx.recyclerview.selection.SelectionStorage;
+
+/**
+ * ContentPager demo activity.
+ */
+public class FancySelectionDemoActivity extends AppCompatActivity {
+
+ private static final String TAG = "SelectionDemos";
+ private static final String EXTRA_COLUMN_COUNT = "demo-column-count";
+
+ private FancySelectionDemoAdapter mAdapter;
+ private SelectionHelper<Uri> mSelectionHelper;
+ private SelectionStorage<Uri> mSelectionStorage;
+
+ private GridLayoutManager mLayout;
+ private int mColumnCount = 1; // This will get updated when layout changes.
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.selection_demo_layout);
+ RecyclerView recView = (RecyclerView) findViewById(R.id.list);
+
+ mLayout = new GridLayoutManager(this, mColumnCount);
+ recView.setLayoutManager(mLayout);
+ mAdapter = new FancySelectionDemoAdapter(this);
+ recView.setAdapter(mAdapter);
+ ItemKeyProvider<Uri> keyProvider = mAdapter.getItemKeyProvider();
+
+ SelectionHelperBuilder<Uri> builder = new SelectionHelperBuilder<>(
+ recView,
+ keyProvider,
+ new FancyDetailsLookup(recView));
+
+ // Override default behaviors and build in multi select mode.
+ // Call .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+ // for single selection mode.
+ mSelectionHelper = builder
+ .withSelectionPredicate(SelectionPredicates.selectAnything())
+ .withTouchCallbacks(new TouchCallbacks(this))
+ .withMouseCallbacks(new MouseCallbacks(this))
+ .withActivationCallbacks(new ActivationCallbacks(this))
+ .withFocusCallbacks(new FocusCallbacks(this))
+ .withBandOverlay(R.drawable.selection_demo_band_overlay)
+ .build();
+
+ // Provide glue between activity lifecycle and selection for purposes
+ // restoring selection.
+ mSelectionStorage = new SelectionStorage<>(
+ SelectionStorage.TYPE_STRING, mSelectionHelper);
+
+ // Lazily bind SelectionHelper. Allows us to defer initialization of the
+ // SelectionHelper dependency until after the adapter is created.
+ mAdapter.bindSelectionHelper(mSelectionHelper);
+
+ // TODO: Glue selection to ActionMode, since that'll be a common practice.
+ mSelectionHelper.addObserver(
+ new SelectionObserver<Long>() {
+ @Override
+ public void onSelectionChanged() {
+ Log.i(TAG, "Selection changed to: " + mSelectionHelper.getSelection());
+ }
+ });
+
+ // Restore selection from saved state.
+ updateFromSavedState(savedInstanceState);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ mSelectionStorage.onSaveInstanceState(state);
+ state.putInt(EXTRA_COLUMN_COUNT, mColumnCount);
+ }
+
+ private void updateFromSavedState(Bundle state) {
+ mSelectionStorage.onRestoreInstanceState(state);
+
+ if (state != null) {
+ if (state.containsKey(EXTRA_COLUMN_COUNT)) {
+ mColumnCount = state.getInt(EXTRA_COLUMN_COUNT);
+ mLayout.setSpanCount(mColumnCount);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ boolean showMenu = super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.selection_demo_actions, menu);
+ return showMenu;
+ }
+
+ @Override
+ @CallSuper
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.option_menu_add_column).setEnabled(mColumnCount <= 3);
+ menu.findItem(R.id.option_menu_remove_column).setEnabled(mColumnCount > 1);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.option_menu_add_column:
+ // TODO: Add columns
+ mLayout.setSpanCount(++mColumnCount);
+ return true;
+
+ case R.id.option_menu_remove_column:
+ mLayout.setSpanCount(--mColumnCount);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSelectionHelper.clear()) {
+ return;
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private static void toast(Context context, String msg) {
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mSelectionHelper.clearSelection();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mAdapter.loadData();
+ }
+
+ // Implementation of MouseInputHandler.Callbacks allows handling
+ // of higher level events, like onActivated.
+ private static final class ActivationCallbacks extends
+ androidx.recyclerview.selection.ActivationCallbacks<Uri> {
+
+ private final Context mContext;
+
+ ActivationCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean onItemActivated(ItemDetails<Uri> item, MotionEvent e) {
+ toast(mContext, "Activate item: " + item.getSelectionKey());
+ return true;
+ }
+ }
+
+ private static final class FocusCallbacks extends
+ androidx.recyclerview.selection.FocusCallbacks<Uri> {
+
+ private final Context mContext;
+
+ private FocusCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void focusItem(ItemDetails<Uri> item) {
+ toast(mContext, "Focused item: " + item.getSelectionKey());
+ }
+
+ @Override
+ public boolean hasFocusedItem() {
+ return false;
+ }
+
+ @Override
+ public int getFocusedPosition() {
+ return 0;
+ }
+
+ @Override
+ public void clearFocus() {
+ toast(mContext, "Cleared focus.");
+ }
+ }
+
+ // Implementation of MouseInputHandler.Callbacks allows handling
+ // of higher level events, like onActivated.
+ private static final class MouseCallbacks extends
+ androidx.recyclerview.selection.MouseCallbacks {
+
+ private final Context mContext;
+
+ MouseCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean onContextClick(MotionEvent e) {
+ toast(mContext, "Context click received.");
+ return true;
+ }
+ };
+
+ private static final class TouchCallbacks extends
+ androidx.recyclerview.selection.TouchCallbacks {
+
+ private final Context mContext;
+
+ private TouchCallbacks(Context context) {
+ mContext = context;
+ }
+
+ public boolean onDragInitiated(MotionEvent e) {
+ toast(mContext, "onDragInitiated received.");
+ return true;
+ }
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java
new file mode 100644
index 0000000..204ec9e
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.widget.selection.fancy;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+
+final class FancySelectionDemoAdapter extends RecyclerView.Adapter<FancyHolder> {
+
+ private final ItemKeyProvider<Uri> mKeyProvider;
+ private final Context mContext;
+
+ // This should be replaced at "bind" time with a real test that
+ // asks SelectionHelper.
+ private SelectionTest mSelTest;
+
+ FancySelectionDemoAdapter(Context context) {
+ mContext = context;
+ mKeyProvider = new ContentUriKeyProvider("cheeses", Cheeses.sCheeseStrings);
+ mSelTest = new SelectionTest() {
+ @Override
+ public boolean isSelected(Uri id) {
+ throw new IllegalStateException(
+ "Adapter must be initialized with SelectionHelper.");
+ }
+ };
+
+ // In the fancy edition of selection support we supply access to stable
+ // ids using content URI. Since we can map between position and selection key
+ // at will we get fancy dependent functionality like band selection and range support.
+ setHasStableIds(false);
+ }
+
+ ItemKeyProvider<Uri> getItemKeyProvider() {
+ return mKeyProvider;
+ }
+
+ // Glue together SelectionHelper and the adapter.
+ public void bindSelectionHelper(final SelectionHelper<Uri> selectionHelper) {
+ checkArgument(selectionHelper != null);
+ mSelTest = new SelectionTest() {
+ @Override
+ public boolean isSelected(Uri id) {
+ return selectionHelper.isSelected(id);
+ }
+ };
+ }
+
+ void loadData() {
+ onDataReady();
+ }
+
+ private void onDataReady() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return Cheeses.sCheeseStrings.length;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public void onBindViewHolder(FancyHolder holder, int position) {
+ Uri uri = mKeyProvider.getKey(position);
+ holder.update(uri, uri.getLastPathSegment(), mSelTest.isSelected(uri));
+ }
+
+ @Override
+ public FancyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LinearLayout layout = inflateLayout(mContext, parent, R.layout.selection_demo_list_item);
+ return new FancyHolder(layout);
+ }
+
+ @SuppressWarnings("TypeParameterUnusedInFormals") // Convenience to avoid clumsy cast.
+ private static <V extends View> V inflateLayout(
+ Context context, ViewGroup parent, int layout) {
+
+ return (V) LayoutInflater.from(context).inflate(layout, parent, false);
+ }
+
+ private interface SelectionTest {
+ boolean isSelected(Uri id);
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java
new file mode 100644
index 0000000..15aa086
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.widget.selection.simple;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Access to details of an item associated with a {@link MotionEvent} instance.
+ */
+final class DemoDetailsLookup extends ItemDetailsLookup<Long> {
+
+ private final RecyclerView mRecView;
+
+ DemoDetailsLookup(RecyclerView view) {
+ mRecView = view;
+ }
+
+ @Override
+ public ItemDetails<Long> getItemDetails(MotionEvent e) {
+ @Nullable View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+ if (view != null) {
+ ViewHolder holder = mRecView.getChildViewHolder(view);
+ if (holder instanceof DemoHolder) {
+ return ((DemoHolder) holder).getItemDetails();
+ }
+ }
+ return null;
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java
new file mode 100644
index 0000000..c1d8b99
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.widget.selection.simple;
+
+import android.graphics.Rect;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+final class DemoHolder extends RecyclerView.ViewHolder {
+
+ private final LinearLayout mContainer;
+ private final TextView mSelector;
+ private final TextView mLabel;
+ private final ItemDetails<Long> mDetails;
+ private @Nullable Long mKey;
+
+ DemoHolder(LinearLayout layout) {
+ super(layout);
+ mContainer = layout.findViewById(R.id.container);
+ mSelector = layout.findViewById(R.id.selector);
+ mLabel = layout.findViewById(R.id.label);
+ mDetails = new ItemDetails<Long>() {
+ @Override
+ public int getPosition() {
+ return DemoHolder.this.getAdapterPosition();
+ }
+
+ @Override
+ public Long getSelectionKey() {
+ return DemoHolder.this.getItemId();
+ }
+
+ @Override
+ public boolean inDragRegion(MotionEvent e) {
+ return DemoHolder.this.inDragRegion(e);
+ }
+
+ @Override
+ public boolean inSelectionHotspot(MotionEvent e) {
+ return DemoHolder.this.inSelectRegion(e);
+ }
+ };
+ }
+
+ void update(String label, boolean selected) {
+ mLabel.setText(label);
+ setSelected(selected);
+ }
+
+ private void setSelected(boolean selected) {
+ mContainer.setActivated(selected);
+ mSelector.setActivated(selected);
+ }
+
+ boolean inDragRegion(MotionEvent event) {
+ // If itemView is activated = selected, then whole region is interactive
+ if (itemView.isActivated()) {
+ return true;
+ }
+
+ // Do everything in global coordinates - it makes things simpler.
+ int[] coords = new int[2];
+ mSelector.getLocationOnScreen(coords);
+
+ Rect textBounds = new Rect();
+ mLabel.getPaint().getTextBounds(
+ mLabel.getText().toString(), 0, mLabel.getText().length(), textBounds);
+
+ Rect rect = new Rect(
+ coords[0],
+ coords[1],
+ coords[0] + mSelector.getWidth() + textBounds.width(),
+ coords[1] + Math.max(mSelector.getHeight(), textBounds.height()));
+
+ // If the tap occurred inside icon or the text, these are interactive spots.
+ return rect.contains((int) event.getRawX(), (int) event.getRawY());
+ }
+
+ boolean inSelectRegion(MotionEvent e) {
+ Rect iconRect = new Rect();
+ mSelector.getGlobalVisibleRect(iconRect);
+ return iconRect.contains((int) e.getRawX(), (int) e.getRawY());
+ }
+
+ ItemDetails<Long> getItemDetails() {
+ return mDetails;
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java
new file mode 100644
index 0000000..74d2fcc
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.widget.selection.simple;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+import androidx.recyclerview.selection.SelectionHelperBuilder;
+import androidx.recyclerview.selection.SelectionPredicates;
+import androidx.recyclerview.selection.SelectionStorage;
+import androidx.recyclerview.selection.StableIdKeyProvider;
+
+/**
+ * ContentPager demo activity.
+ */
+public class SimpleSelectionDemoActivity extends AppCompatActivity {
+
+ private static final String TAG = "SelectionDemos";
+ private static final String EXTRA_COLUMN_COUNT = "demo-column-count";
+
+ private SimpleSelectionDemoAdapter mAdapter;
+ private SelectionHelper<Long> mSelectionHelper;
+ private SelectionStorage<Long> mSelectionStorage;
+
+ private GridLayoutManager mLayout;
+ private int mColumnCount = 1; // This will get updated when layout changes.
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.selection_demo_layout);
+ RecyclerView recView = (RecyclerView) findViewById(R.id.list);
+
+ // keyProvider depends on mAdapter.setHasStableIds(true).
+ ItemKeyProvider<Long> keyProvider = new StableIdKeyProvider(recView);
+
+ mLayout = new GridLayoutManager(this, mColumnCount);
+ recView.setLayoutManager(mLayout);
+
+ mAdapter = new SimpleSelectionDemoAdapter(this, keyProvider);
+ // The adapter is paired with a key provider that supports
+ // the native RecyclerView stableId. For this to work correctly
+ // the adapter must report that it supports stable ids.
+ mAdapter.setHasStableIds(true);
+
+ recView.setAdapter(mAdapter);
+
+ SelectionHelperBuilder<Long> builder = new SelectionHelperBuilder<>(
+ recView,
+ keyProvider,
+ new DemoDetailsLookup(recView));
+
+ // Override default behaviors and build in multi select mode.
+ // Call .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+ // for single selection mode.
+ mSelectionHelper = builder
+ .withSelectionPredicate(SelectionPredicates.selectAnything())
+ .withTouchCallbacks(new TouchCallbacks(this))
+ .withMouseCallbacks(new MouseCallbacks(this))
+ .withActivationCallbacks(new ActivationCallbacks(this))
+ .withFocusCallbacks(new FocusCallbacks(this))
+ .withBandOverlay(R.drawable.selection_demo_band_overlay)
+ .build();
+
+ // Provide glue between activity lifecycle and selection for purposes
+ // restoring selection.
+ mSelectionStorage = new SelectionStorage<>(
+ SelectionStorage.TYPE_STRING, mSelectionHelper);
+
+ // Lazily bind SelectionHelper. Allows us to defer initialization of the
+ // SelectionHelper dependency until after the adapter is created.
+ mAdapter.bindSelectionHelper(mSelectionHelper);
+
+ // TODO: Glue selection to ActionMode, since that'll be a common practice.
+ mSelectionHelper.addObserver(
+ new SelectionObserver<Long>() {
+ @Override
+ public void onSelectionChanged() {
+ Log.i(TAG, "Selection changed to: " + mSelectionHelper.getSelection());
+ }
+ });
+
+ // Restore selection from saved state.
+ updateFromSavedState(savedInstanceState);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ mSelectionStorage.onSaveInstanceState(state);
+ state.putInt(EXTRA_COLUMN_COUNT, mColumnCount);
+ }
+
+ private void updateFromSavedState(Bundle state) {
+ mSelectionStorage.onRestoreInstanceState(state);
+
+ if (state != null) {
+ if (state.containsKey(EXTRA_COLUMN_COUNT)) {
+ mColumnCount = state.getInt(EXTRA_COLUMN_COUNT);
+ mLayout.setSpanCount(mColumnCount);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ boolean showMenu = super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.selection_demo_actions, menu);
+ return showMenu;
+ }
+
+ @Override
+ @CallSuper
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.option_menu_add_column).setEnabled(mColumnCount <= 3);
+ menu.findItem(R.id.option_menu_remove_column).setEnabled(mColumnCount > 1);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.option_menu_add_column:
+ // TODO: Add columns
+ mLayout.setSpanCount(++mColumnCount);
+ return true;
+
+ case R.id.option_menu_remove_column:
+ mLayout.setSpanCount(--mColumnCount);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSelectionHelper.clear()) {
+ return;
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private static void toast(Context context, String msg) {
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mSelectionHelper.clearSelection();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mAdapter.loadData();
+ }
+
+ // Implementation of MouseInputHandler.Callbacks allows handling
+ // of higher level events, like onActivated.
+ private static final class ActivationCallbacks extends
+ androidx.recyclerview.selection.ActivationCallbacks<Long> {
+
+ private final Context mContext;
+
+ ActivationCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean onItemActivated(ItemDetails<Long> item, MotionEvent e) {
+ toast(mContext, "Activate item: " + item.getSelectionKey());
+ return true;
+ }
+ }
+
+ private static final class FocusCallbacks extends
+ androidx.recyclerview.selection.FocusCallbacks<Long> {
+
+ private final Context mContext;
+
+ private FocusCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void focusItem(ItemDetails<Long> item) {
+ toast(mContext, "Focused item: " + item.getSelectionKey());
+ }
+
+ @Override
+ public boolean hasFocusedItem() {
+ return false;
+ }
+
+ @Override
+ public int getFocusedPosition() {
+ return 0;
+ }
+
+ @Override
+ public void clearFocus() {
+ toast(mContext, "Cleared focus.");
+ }
+ }
+
+ // Implementation of MouseInputHandler.Callbacks allows handling
+ // of higher level events, like onActivated.
+ private static final class MouseCallbacks extends
+ androidx.recyclerview.selection.MouseCallbacks {
+
+ private final Context mContext;
+
+ MouseCallbacks(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean onContextClick(MotionEvent e) {
+ toast(mContext, "Context click received.");
+ return true;
+ }
+ };
+
+ private static final class TouchCallbacks extends
+ androidx.recyclerview.selection.TouchCallbacks {
+
+ private final Context mContext;
+
+ private TouchCallbacks(Context context) {
+ mContext = context;
+ }
+
+ public boolean onDragInitiated(MotionEvent e) {
+ toast(mContext, "onDragInitiated received.");
+ return true;
+ }
+ }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java
new file mode 100644
index 0000000..a60fda8
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.widget.selection.simple;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+
+final class SimpleSelectionDemoAdapter extends RecyclerView.Adapter<DemoHolder> {
+
+ private static final String TAG = "SelectionDemos";
+ private final Context mContext;
+ private final ItemKeyProvider<Long> mKeyProvider;
+
+ // This should be replaced at "bind" time with a real test that
+ // asks SelectionHelper.
+ private SelectionTest mSelTest;
+
+ SimpleSelectionDemoAdapter(Context context, ItemKeyProvider<Long> keyProvider) {
+ mContext = context;
+ mKeyProvider = keyProvider;
+ mSelTest = new SelectionTest() {
+ @Override
+ public boolean isSelected(Long id) {
+ throw new IllegalStateException(
+ "Adapter must be initialized with SelectionHelper.");
+ }
+ };
+ }
+
+ // Glue together SelectionHelper and the adapter.
+ public void bindSelectionHelper(final SelectionHelper<Long> selectionHelper) {
+ checkArgument(selectionHelper != null);
+ mSelTest = new SelectionTest() {
+ @Override
+ public boolean isSelected(Long id) {
+ return selectionHelper.isSelected(id);
+ }
+ };
+ }
+
+ void loadData() {
+ onDataReady();
+ }
+
+ private void onDataReady() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return Cheeses.sCheeseStrings.length;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public void onBindViewHolder(DemoHolder holder, int position) {
+ Long key = getItemId(position);
+ Log.v(TAG, "Just before rendering item position=" + position + ", key=" + key);
+ holder.update(Cheeses.sCheeseStrings[position], mSelTest.isSelected(key));
+ }
+
+ @Override
+ public DemoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LinearLayout layout = inflateLayout(mContext, parent, R.layout.selection_demo_list_item);
+ return new DemoHolder(layout);
+ }
+
+ @SuppressWarnings("TypeParameterUnusedInFormals") // Convenience to avoid clumsy cast.
+ private static <V extends View> V inflateLayout(
+ Context context, ViewGroup parent, int layout) {
+
+ return (V) LayoutInflater.from(context).inflate(layout, parent, false);
+ }
+
+ private interface SelectionTest {
+ boolean isSelected(Long id);
+ }
+}
diff --git a/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
new file mode 100644
index 0000000..bd87b4c
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_activated="true"
+ android:color="?android:attr/colorForeground"
+ />
+ <item
+ android:state_activated="false"
+ android:color="?android:attr/colorForeground"
+ android:alpha=".3"
+ />
+</selector>
diff --git a/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.xml b/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.xml
new file mode 100644
index 0000000..f9793aa
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="#22FF0000" />
+ <stroke android:width="1dp" android:color="#44FF0000" />
+</shape>
diff --git a/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.xml b/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.xml
new file mode 100644
index 0000000..e4dbd5f
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_activated="true">
+ <color android:color="#220000FF"></color>
+ </item>
+</selector>
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
new file mode 100644
index 0000000..bd85a14
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:drawSelectorOnTop="true"
+ android:paddingBottom="5dp"
+ android:paddingEnd="0dp"
+ android:paddingStart="0dp"
+ android:paddingTop="5dp"
+ android:scrollbars="none" />
+
+</LinearLayout>
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
new file mode 100644
index 0000000..0d4b718
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:paddingStart="10dp"
+ android:paddingEnd="10dp"
+ android:paddingTop="5dp"
+ android:paddingBottom="5dp"
+ android:layout_height="50dp">
+ <LinearLayout
+ android:id="@+id/container"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:background="@drawable/selection_demo_item_background">
+ <TextView
+ android:id="@+id/selector"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:gravity="center"
+ android:layout_height="match_parent"
+ android:layout_width="40dp"
+ android:textColor="@color/selection_demo_item_selector"
+ android:pointerIcon="hand"
+ android:text="✕">
+ </TextView>
+ <TextView
+ android:id="@+id/label"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:gravity="center_vertical"
+ android:paddingStart="10dp"
+ android:paddingEnd="10dp"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+ </TextView>
+ </LinearLayout>
+</LinearLayout>
diff --git a/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
new file mode 100644
index 0000000..484d8b6
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/option_menu_add_column"
+ android:title="Add column" />
+ <item
+ android:id="@+id/option_menu_remove_column"
+ android:title="Remove column" />
+</menu>
diff --git a/samples/Support7Demos/src/main/res/values/strings.xml b/samples/Support7Demos/src/main/res/values/strings.xml
index 097807f..2c4b0b6 100644
--- a/samples/Support7Demos/src/main/res/values/strings.xml
+++ b/samples/Support7Demos/src/main/res/values/strings.xml
@@ -233,10 +233,12 @@
<string name="popup_menu_print">Print</string>
<string name="list_view_activity">AppCompat/ListView styling</string>
-
<string name="appcompat_vector_disabled">AnimatedVectorDrawableCompat does not work on devices running API v10 or below</string>
<string name="appcompat_vector_title">AppCompat/Integrations/AnimatedVectorDrawable</string>
+ <string name="simple_selection_demo_activity">RecyclerView Selection</string>
+ <string name="fancy_selection_demo_activity">RecyclerView: Gesture+Pointer Selection</string>
+
<string name="night_mode">DAY</string>
<string name="text_plain_enabled">Plain enabled</string>
@@ -246,4 +248,3 @@
<string name="menu_item_icon_tinting">AppCompat/Menu Item Icons</string>
</resources>
-
diff --git a/samples/SupportDesignDemos/src/main/res/layout/design_fab.xml b/samples/SupportDesignDemos/src/main/res/layout/design_fab.xml
index ad26d5b..f00dd96 100644
--- a/samples/SupportDesignDemos/src/main/res/layout/design_fab.xml
+++ b/samples/SupportDesignDemos/src/main/res/layout/design_fab.xml
@@ -66,6 +66,21 @@
android:clickable="true"
app:fabSize="mini" />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title"
+ android:text="@string/fab_size_custom" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/custom_fab"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_margin="16dp"
+ android:src="@drawable/ic_add"
+ android:clickable="true"
+ app:fabCustomSize="@dimen/custom_fab_size" />
</LinearLayout>
</FrameLayout>
\ No newline at end of file
diff --git a/samples/SupportDesignDemos/src/main/res/values/dimens.xml b/samples/SupportDesignDemos/src/main/res/values/dimens.xml
index c8a5ea9..68e4775 100644
--- a/samples/SupportDesignDemos/src/main/res/values/dimens.xml
+++ b/samples/SupportDesignDemos/src/main/res/values/dimens.xml
@@ -20,4 +20,6 @@
<dimen name="bottom_sheet_peek_height">128dp</dimen>
<dimen name="custom_snackbar_max_width">-1px</dimen>
+
+ <dimen name="custom_fab_size">45dp</dimen>
</resources>
diff --git a/samples/SupportDesignDemos/src/main/res/values/strings.xml b/samples/SupportDesignDemos/src/main/res/values/strings.xml
index 6ebe24c..e1818af 100644
--- a/samples/SupportDesignDemos/src/main/res/values/strings.xml
+++ b/samples/SupportDesignDemos/src/main/res/values/strings.xml
@@ -37,6 +37,7 @@
<string name="fab_size_normal">Normal size</string>
<string name="fab_size_mini">Mini size</string>
+ <string name="fab_size_custom">Custom size</string>
<string name="navigation_open">Open</string>
<string name="navigation_close">Close</string>
diff --git a/samples/SupportEmojiDemos/OWNERS b/samples/SupportEmojiDemos/OWNERS
new file mode 100644
index 0000000..a2db8f4
--- /dev/null
+++ b/samples/SupportEmojiDemos/OWNERS
@@ -0,0 +1 @@
+siyamed@google.com
\ No newline at end of file
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackDemos/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackDemos/OWNERS
diff --git a/samples/SupportLeanbackDemos/generatev4.py b/samples/SupportLeanbackDemos/generatev4.py
index 6a44e17..3ffec76 100755
--- a/samples/SupportLeanbackDemos/generatev4.py
+++ b/samples/SupportLeanbackDemos/generatev4.py
@@ -25,8 +25,8 @@
def replace_xml_head(line, name):
return line.replace('<?xml version="1.0" encoding="utf-8"?>', '<?xml version="1.0" encoding="utf-8"?>\n<!-- This file is auto-generated from {}.xml. DO NOT MODIFY. -->\n'.format(name))
-file = open('src/com/example/android/leanback/GuidedStepActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/GuidedStepSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/GuidedStepActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java', 'w')
write_java_head(outfile, "GuidedStepActivity")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -38,8 +38,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/GuidedStepHalfScreenActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java', 'w')
write_java_head(outfile, "GuidedStepHalfScreenActivity")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -52,8 +52,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/BrowseFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseSupportFragment.java', 'w')
write_java_head(outfile, "BrowseFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -72,8 +72,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/BrowseActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseSupportActivity.java', 'w')
write_java_head(outfile, "BrowseActivity")
for line in file:
line = line.replace('BrowseActivity', 'BrowseSupportActivity')
@@ -84,8 +84,8 @@
file.close()
outfile.close()
-file = open('res/layout/browse.xml', 'r')
-outfile = open('res/layout/browse_support.xml', 'w')
+file = open('src/main/res/layout/browse.xml', 'r')
+outfile = open('src/main/res/layout/browse_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "browse")
line = line.replace('com.example.android.leanback.BrowseFragment', 'com.example.android.leanback.BrowseSupportFragment')
@@ -94,8 +94,8 @@
outfile.close()
-file = open('src/com/example/android/leanback/DetailsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/DetailsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/DetailsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/DetailsSupportFragment.java', 'w')
write_java_head(outfile, "DetailsFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -108,8 +108,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/NewDetailsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/NewDetailsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/NewDetailsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java', 'w')
write_java_head(outfile, "NewDetailsFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -127,8 +127,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/DetailsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/DetailsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/DetailsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/DetailsSupportActivity.java', 'w')
write_java_head(outfile, "DetailsActivity")
for line in file:
line = line.replace('DetailsActivity', 'DetailsSupportActivity')
@@ -141,8 +141,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/SearchDetailsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchDetailsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
write_java_head(outfile, "SearchDetailsActivity")
for line in file:
line = line.replace('DetailsActivity', 'DetailsSupportActivity')
@@ -151,8 +151,8 @@
outfile.close()
-file = open('src/com/example/android/leanback/SearchFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchSupportFragment.java', 'w')
write_java_head(outfile, "SearchFragment")
for line in file:
line = line.replace('SearchFragment', 'SearchSupportFragment')
@@ -161,8 +161,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/SearchActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchSupportActivity.java', 'w')
write_java_head(outfile, "SearchActivity")
for line in file:
line = line.replace('SearchActivity', 'SearchSupportActivity')
@@ -175,8 +175,8 @@
file.close()
outfile.close()
-file = open('res/layout/search.xml', 'r')
-outfile = open('res/layout/search_support.xml', 'w')
+file = open('src/main/res/layout/search.xml', 'r')
+outfile = open('src/main/res/layout/search_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "search")
line = line.replace('com.example.android.leanback.SearchFragment', 'com.example.android.leanback.SearchSupportFragment')
@@ -184,8 +184,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/VerticalGridFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/VerticalGridSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VerticalGridFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VerticalGridSupportFragment.java', 'w')
write_java_head(outfile, "VerticalGridFragment")
for line in file:
line = line.replace('VerticalGridFragment', 'VerticalGridSupportFragment')
@@ -195,8 +195,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/VerticalGridActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/VerticalGridSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VerticalGridActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VerticalGridSupportActivity.java', 'w')
write_java_head(outfile, "VerticalGridActivity")
for line in file:
line = line.replace('VerticalGridActivity', 'VerticalGridSupportActivity')
@@ -209,8 +209,8 @@
file.close()
outfile.close()
-file = open('res/layout/vertical_grid.xml', 'r')
-outfile = open('res/layout/vertical_grid_support.xml', 'w')
+file = open('src/main/res/layout/vertical_grid.xml', 'r')
+outfile = open('src/main/res/layout/vertical_grid_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "vertical_grid")
line = line.replace('com.example.android.leanback.VerticalGridFragment', 'com.example.android.leanback.VerticalGridSupportFragment')
@@ -219,8 +219,8 @@
outfile.close()
-file = open('src/com/example/android/leanback/ErrorFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/ErrorSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/ErrorFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/ErrorSupportFragment.java', 'w')
write_java_head(outfile, "ErrorFragment")
for line in file:
line = line.replace('ErrorFragment', 'ErrorSupportFragment')
@@ -228,8 +228,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/BrowseErrorActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseErrorSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseErrorActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseErrorSupportActivity.java', 'w')
write_java_head(outfile, "BrowseErrorActivity")
for line in file:
line = line.replace('BrowseErrorActivity', 'BrowseErrorSupportActivity')
@@ -244,8 +244,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/RowsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/RowsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/RowsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/RowsSupportFragment.java', 'w')
write_java_head(outfile, "RowsFragment")
for line in file:
line = line.replace('RowsFragment', 'RowsSupportFragment')
@@ -254,8 +254,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/RowsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/RowsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/RowsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/RowsSupportActivity.java', 'w')
write_java_head(outfile, "RowsActivity")
for line in file:
line = line.replace('RowsActivity', 'RowsSupportActivity')
@@ -269,8 +269,8 @@
file.close()
outfile.close()
-file = open('res/layout/rows.xml', 'r')
-outfile = open('res/layout/rows_support.xml', 'w')
+file = open('src/main/res/layout/rows.xml', 'r')
+outfile = open('src/main/res/layout/rows_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "rows")
line = line.replace('com.example.android.leanback.RowsFragment', 'com.example.android.leanback.RowsSupportFragment')
@@ -278,8 +278,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/PlaybackFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackSupportFragment.java', 'w')
write_java_head(outfile, "PlaybackFragment")
for line in file:
line = line.replace('PlaybackFragment', 'PlaybackSupportFragment')
@@ -288,8 +288,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/PlaybackActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackSupportActivity.java', 'w')
write_java_head(outfile, "PlaybackActivity")
for line in file:
line = line.replace('PlaybackActivity', 'PlaybackSupportActivity')
@@ -300,8 +300,8 @@
file.close()
outfile.close()
-file = open('res/layout/playback_activity.xml', 'r')
-outfile = open('res/layout/playback_activity_support.xml', 'w')
+file = open('src/main/res/layout/playback_activity.xml', 'r')
+outfile = open('src/main/res/layout/playback_activity_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "playback_controls")
line = line.replace('com.example.android.leanback.PlaybackFragment', 'com.example.android.leanback.PlaybackSupportFragment')
@@ -309,8 +309,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/PlaybackTransportControlFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackTransportControlSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackTransportControlFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackTransportControlSupportFragment.java', 'w')
write_java_head(outfile, "PlaybackTransportControlFragment")
for line in file:
line = line.replace('PlaybackFragment', 'PlaybackSupportFragment')
@@ -320,8 +320,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/PlaybackTransportControlActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackTransportControlSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackTransportControlActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackTransportControlSupportActivity.java', 'w')
write_java_head(outfile, "PlaybackTransportControlActivity")
for line in file:
line = line.replace('PlaybackTransportControlActivity', 'PlaybackTransportControlSupportActivity')
@@ -332,8 +332,8 @@
file.close()
outfile.close()
-file = open('res/layout/playback_transportcontrol_activity.xml', 'r')
-outfile = open('res/layout/playback_transportcontrol_activity_support.xml', 'w')
+file = open('src/main/res/layout/playback_transportcontrol_activity.xml', 'r')
+outfile = open('src/main/res/layout/playback_transportcontrol_activity_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "playback_transportcontrols")
line = line.replace('com.example.android.leanback.PlaybackTransportControlFragment', 'com.example.android.leanback.PlaybackTransportControlSupportFragment')
@@ -341,45 +341,8 @@
file.close()
outfile.close()
-
-
-file = open('src/com/example/android/leanback/PlaybackOverlayFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackOverlaySupportFragment.java', 'w')
-write_java_head(outfile, "PlaybackOverlayFragment")
-for line in file:
- line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
- line = line.replace('PlaybackControlHelper', 'PlaybackControlSupportHelper')
- line = line.replace('PlaybackOverlayActivity', 'PlaybackOverlaySupportActivity')
- outfile.write(line)
-file.close()
-outfile.close()
-
-
-file = open('src/com/example/android/leanback/PlaybackControlHelper.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackControlSupportHelper.java', 'w')
-write_java_head(outfile, "PlaybackControlHelper")
-for line in file:
- line = line.replace('PlaybackControlHelper', 'PlaybackControlSupportHelper')
- line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue')
- line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
- outfile.write(line)
-file.close()
-outfile.close()
-
-file = open('src/com/example/android/leanback/PlaybackOverlayActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackOverlaySupportActivity.java', 'w')
-write_java_head(outfile, "PlaybackOverlayActivity")
-for line in file:
- line = line.replace('PlaybackOverlayActivity', 'PlaybackOverlaySupportActivity')
- line = line.replace('extends Activity', 'extends FragmentActivity')
- line = line.replace('R.layout.playback_controls', 'R.layout.playback_controls_support')
- line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
- outfile.write(line)
-file.close()
-outfile.close()
-
-file = open('res/layout/playback_controls.xml', 'r')
-outfile = open('res/layout/playback_controls_support.xml', 'w')
+file = open('src/main/res/layout/playback_controls.xml', 'r')
+outfile = open('src/main/res/layout/playback_controls_support.xml', 'w')
for line in file:
line = replace_xml_head(line, "playback_controls")
line = line.replace('com.example.android.leanback.PlaybackOverlayFragment', 'com.example.android.leanback.PlaybackOverlaySupportFragment')
@@ -387,8 +350,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/OnboardingActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/OnboardingSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/OnboardingActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/OnboardingSupportActivity.java', 'w')
write_java_head(outfile, "OnboardingActivity")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -401,8 +364,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/OnboardingDemoFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/OnboardingDemoSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/OnboardingDemoFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/OnboardingDemoSupportFragment.java', 'w')
write_java_head(outfile, "OnboardingDemoFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -414,8 +377,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/SampleVideoFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/SampleVideoSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SampleVideoFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SampleVideoSupportFragment.java', 'w')
write_java_head(outfile, "OnboardingDemoFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -426,8 +389,8 @@
file.close()
outfile.close()
-file = open('src/com/example/android/leanback/VideoActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/VideoSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VideoActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VideoSupportActivity.java', 'w')
write_java_head(outfile, "OnboardingDemoFragment")
for line in file:
line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
index 7b3f8f7..eb0b684 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
@@ -144,7 +144,6 @@
ListRowPresenter listRowPresenter = new ListRowPresenter();
listRowPresenter.setNumRows(1);
mRowsAdapter = new ArrayObjectAdapter(listRowPresenter);
- setAdapter(mRowsAdapter);
}
private void loadData() {
@@ -164,6 +163,7 @@
mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID2, "Page Row 1")));
mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID3, "Page Row 2")));
+ setAdapter(mRowsAdapter);
}
private ArrayObjectAdapter createListRowAdapter(int i) {
@@ -273,7 +273,7 @@
final CardPresenter mCardPresenter2 = new CardPresenter(R.style.MyImageCardViewTheme);
void loadFragmentData() {
- ArrayObjectAdapter adapter = (ArrayObjectAdapter) getAdapter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
for (int i = 0; i < 4; i++) {
ListRow row = new ListRow(new HeaderItem("Row " + i), createListRowAdapter(i));
adapter.add(row);
@@ -282,11 +282,10 @@
getMainFragmentAdapter().getFragmentHost()
.notifyDataReady(getMainFragmentAdapter());
}
+ setAdapter(adapter);
}
public SampleRowsFragment() {
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
// simulates late data loading:
new Handler().postDelayed(new Runnable() {
@Override
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
index 395c498..7afd24f 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
@@ -147,7 +147,6 @@
ListRowPresenter listRowPresenter = new ListRowPresenter();
listRowPresenter.setNumRows(1);
mRowsAdapter = new ArrayObjectAdapter(listRowPresenter);
- setAdapter(mRowsAdapter);
}
private void loadData() {
@@ -167,6 +166,7 @@
mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID2, "Page Row 1")));
mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID3, "Page Row 2")));
+ setAdapter(mRowsAdapter);
}
private ArrayObjectAdapter createListRowAdapter(int i) {
@@ -276,7 +276,7 @@
final CardPresenter mCardPresenter2 = new CardPresenter(R.style.MyImageCardViewTheme);
void loadFragmentData() {
- ArrayObjectAdapter adapter = (ArrayObjectAdapter) getAdapter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
for (int i = 0; i < 4; i++) {
ListRow row = new ListRow(new HeaderItem("Row " + i), createListRowAdapter(i));
adapter.add(row);
@@ -285,11 +285,10 @@
getMainFragmentAdapter().getFragmentHost()
.notifyDataReady(getMainFragmentAdapter());
}
+ setAdapter(adapter);
}
public SampleRowsSupportFragment() {
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
// simulates late data loading:
new Handler().postDelayed(new Runnable() {
@Override
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
index 0f15590..1af248f 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
@@ -119,7 +119,7 @@
actions.clear(ACTION_RENT);
dor.setItem(mPhotoItem.getTitle() + "(Rented)");
} else if (action.getId() == ACTION_PLAY) {
- Intent intent = new Intent(context, PlaybackSupportActivity.class);
+ Intent intent = new Intent(context, PlaybackActivity.class);
getActivity().startActivity(intent);
}
}
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
index 7f898f4..7d20046 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
@@ -55,6 +55,7 @@
private static final int PAYMENT = 6;
private static final int NEW_PAYMENT = 7;
private static final int PAYMENT_EXPIRE = 8;
+ private static final int REFRESH = 9;
private static final long RADIO_ID_BASE = 0;
private static final long CHECKBOX_ID_BASE = 100;
@@ -222,6 +223,10 @@
.description("Let's do it")
.build());
actions.add(new GuidedAction.Builder(context)
+ .id(REFRESH)
+ .title("Refresh")
+ .build());
+ actions.add(new GuidedAction.Builder(context)
.clickAction(GuidedAction.ACTION_ID_CANCEL)
.description("Never mind")
.build());
@@ -232,6 +237,24 @@
FragmentManager fm = getFragmentManager();
if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
GuidedStepFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host);
+ } else if (action.getId() == REFRESH) {
+ // swap actions position and change content:
+ Context context = getActivity();
+ ArrayList<GuidedAction> newActions = new ArrayList();
+ newActions.add(new GuidedAction.Builder(context)
+ .id(REFRESH)
+ .title("Refresh done")
+ .build());
+ newActions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_CONTINUE)
+ .description("Let's do it")
+ .build());
+ newActions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .description("Never mind")
+ .build());
+ //setActionsDiffCallback(null);
+ setActions(newActions);
} else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){
finishGuidedStepFragments();
}
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
index c0f9361..6782b63 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
@@ -58,6 +58,7 @@
private static final int PAYMENT = 6;
private static final int NEW_PAYMENT = 7;
private static final int PAYMENT_EXPIRE = 8;
+ private static final int REFRESH = 9;
private static final long RADIO_ID_BASE = 0;
private static final long CHECKBOX_ID_BASE = 100;
@@ -225,6 +226,10 @@
.description("Let's do it")
.build());
actions.add(new GuidedAction.Builder(context)
+ .id(REFRESH)
+ .title("Refresh")
+ .build());
+ actions.add(new GuidedAction.Builder(context)
.clickAction(GuidedAction.ACTION_ID_CANCEL)
.description("Never mind")
.build());
@@ -235,6 +240,24 @@
FragmentManager fm = getFragmentManager();
if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
GuidedStepSupportFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host);
+ } else if (action.getId() == REFRESH) {
+ // swap actions position and change content:
+ Context context = getActivity();
+ ArrayList<GuidedAction> newActions = new ArrayList();
+ newActions.add(new GuidedAction.Builder(context)
+ .id(REFRESH)
+ .title("Refresh done")
+ .build());
+ newActions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_CONTINUE)
+ .description("Let's do it")
+ .build());
+ newActions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .description("Never mind")
+ .build());
+ //setActionsDiffCallback(null);
+ setActions(newActions);
} else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){
finishGuidedStepSupportFragments();
}
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
index 6002cf3..b2ff5b2 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
@@ -178,7 +178,7 @@
mDetailsBackground.switchToVideo();
}
} else {
- Intent intent = new Intent(context, PlaybackSupportActivity.class);
+ Intent intent = new Intent(context, PlaybackActivity.class);
getActivity().startActivity(intent);
}
} else if (action.getId() == ACTION_RENT) {
@@ -193,14 +193,14 @@
setupMainVideo();
mDetailsBackground.switchToVideo();
} else {
- Intent intent = new Intent(context, PlaybackSupportActivity.class);
+ Intent intent = new Intent(context, PlaybackActivity.class);
getActivity().startActivity(intent);
}
} else if (action.getId() == ACTION_PLAY) {
if (TEST_BACKGROUND_PLAYER) {
mDetailsBackground.switchToVideo();
} else {
- Intent intent = new Intent(context, PlaybackSupportActivity.class);
+ Intent intent = new Intent(context, PlaybackActivity.class);
getActivity().startActivity(intent);
}
}
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackJank/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackJank/OWNERS
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackShowcase/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackShowcase/OWNERS
diff --git a/samples/SupportLeanbackShowcase/build.gradle b/samples/SupportLeanbackShowcase/build.gradle
index 74f1e76..287a234 100644
--- a/samples/SupportLeanbackShowcase/build.gradle
+++ b/samples/SupportLeanbackShowcase/build.gradle
@@ -18,7 +18,7 @@
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.1'
+ classpath 'com.android.tools.build:gradle:3.0.0-rc1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/samples/SupportWearDemos/OWNERS b/samples/SupportWearDemos/OWNERS
new file mode 100644
index 0000000..9cd6e52
--- /dev/null
+++ b/samples/SupportWearDemos/OWNERS
@@ -0,0 +1,2 @@
+amad@google.com
+griff@google.com
\ No newline at end of file
diff --git a/samples/SupportWearDemos/build.gradle b/samples/SupportWearDemos/build.gradle
index 99223f9..ae0f195 100644
--- a/samples/SupportWearDemos/build.gradle
+++ b/samples/SupportWearDemos/build.gradle
@@ -18,6 +18,7 @@
dependencies {
implementation project(':wear')
+ implementation project(path: ':appcompat-v7')
}
android {
diff --git a/samples/SupportWearDemos/src/main/AndroidManifest.xml b/samples/SupportWearDemos/src/main/AndroidManifest.xml
index 957a539..b70982b 100644
--- a/samples/SupportWearDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportWearDemos/src/main/AndroidManifest.xml
@@ -29,6 +29,8 @@
<activity android:name=".app.RoundedDrawableDemo" />
<activity android:name=".app.drawers.WearableDrawersDemo" android:exported="true" />
<activity android:name=".app.AmbientModeDemo" />
+ <activity android:name=".app.AlertDialogDemo"
+ android:theme="@style/Theme.AppCompat.Light" />
<activity android:name=".app.MainDemoActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java
new file mode 100644
index 0000000..4ea448a
--- /dev/null
+++ b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.support.wear.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+
+import com.example.android.support.wear.R;
+
+/**
+ * Demo for AlertDialog on Wear.
+ */
+public class AlertDialogDemo extends Activity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.alert_dialog_demo);
+
+ AlertDialog v7Dialog = createV7Dialog();
+ android.app.AlertDialog frameworkDialog = createFrameworkDialog();
+
+ Button v7Trigger = findViewById(R.id.v7_dialog_button);
+ v7Trigger.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ v7Dialog.show();
+ }
+ });
+
+ Button frameworkTrigger = findViewById(R.id.framework_dialog_button);
+ frameworkTrigger.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ frameworkDialog.show();
+ }
+ });
+ }
+
+ private AlertDialog createV7Dialog() {
+ return new AlertDialog.Builder(this)
+ .setTitle("AppCompatDialog")
+ .setMessage("Lorem ipsum dolor...")
+ .setPositiveButton("Ok", null)
+ .setNegativeButton("Cancel", null)
+ .create();
+ }
+
+ private android.app.AlertDialog createFrameworkDialog() {
+ return new android.app.AlertDialog.Builder(this)
+ .setTitle("FrameworkDialog")
+ .setMessage("Lorem ipsum dolor...")
+ .setPositiveButton("Ok", null)
+ .setNegativeButton("Cancel", null)
+ .create();
+ }
+}
diff --git a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
index 0227559..3c50d92 100644
--- a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
+++ b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
@@ -29,7 +29,7 @@
import com.example.android.support.wear.app.drawers.WearableDrawersDemo;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -51,7 +51,7 @@
}
private Map<String, Intent> createContentMap() {
- Map<String, Intent> contentMap = new HashMap<>();
+ Map<String, Intent> contentMap = new LinkedHashMap<>();
contentMap.put("Wearable Recycler View", new Intent(
this, SimpleWearableRecyclerViewDemo.class));
contentMap.put("Wearable Switch", new Intent(
@@ -64,6 +64,8 @@
this, RoundedDrawableDemo.class));
contentMap.put("Ambient Fragment", new Intent(
this, AmbientModeDemo.class));
+ contentMap.put("Alert Dialog (v7)", new Intent(
+ this, AlertDialogDemo.class));
return contentMap;
}
diff --git a/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml b/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml
new file mode 100644
index 0000000..833d489
--- /dev/null
+++ b/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<android.support.wear.widget.BoxInsetLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:boxedEdges="all">
+
+ <Button
+ android:id="@+id/v7_dialog_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Show V7 dialog"/>
+
+ <Button
+ android:id="@+id/framework_dialog_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Show Framework dialog"/>
+ </LinearLayout>
+
+</android.support.wear.widget.BoxInsetLayout>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index c281bb1..0544c91 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,6 +40,9 @@
include ':recyclerview-v7'
project(':recyclerview-v7').projectDir = new File(rootDir, 'v7/recyclerview')
+include ':recyclerview-selection'
+project(':recyclerview-selection').projectDir = new File(rootDir, 'recyclerview-selection')
+
include ':cardview-v7'
project(':cardview-v7').projectDir = new File(rootDir, 'v7/cardview')
@@ -50,13 +53,13 @@
project(':preference-v14').projectDir = new File(rootDir, 'v14/preference')
include ':preference-leanback-v17'
-project(':preference-leanback-v17').projectDir = new File(rootDir, 'v17/preference-leanback')
+project(':preference-leanback-v17').projectDir = new File(rootDir, 'preference-leanback')
include ':support-v13'
project(':support-v13').projectDir = new File(rootDir, 'v13')
include ':leanback-v17'
-project(':leanback-v17').projectDir = new File(rootDir, 'v17/leanback')
+project(':leanback-v17').projectDir = new File(rootDir, 'leanback')
include ':design'
project(':design').projectDir = new File(rootDir, 'design')
@@ -103,6 +106,12 @@
include ':support-content'
project(':support-content').projectDir = new File(rootDir, 'content')
+include ':car'
+project(':car').projectDir = new File(rootDir, 'car')
+
+include ':webkit'
+project(':webkit').projectDir = new File(rootDir, 'webkit')
+
/////////////////////////////
//
// Samples
@@ -172,13 +181,19 @@
/////////////////////////////
include ':support-media-compat-test-client'
-project(':support-media-compat-test-client').projectDir = new File(rootDir, 'media-compat-test-client')
+project(':support-media-compat-test-client').projectDir = new File(rootDir, 'media-compat/version-compat-tests/current/client')
+
+include ':support-media-compat-test-client-previous'
+project(':support-media-compat-test-client-previous').projectDir = new File(rootDir, 'media-compat/version-compat-tests/previous/client')
include ':support-media-compat-test-service'
-project(':support-media-compat-test-service').projectDir = new File(rootDir, 'media-compat-test-service')
+project(':support-media-compat-test-service').projectDir = new File(rootDir, 'media-compat/version-compat-tests/current/service')
+
+include ':support-media-compat-test-service-previous'
+project(':support-media-compat-test-service-previous').projectDir = new File(rootDir, 'media-compat/version-compat-tests/previous/service')
include ':support-media-compat-test-lib'
-project(':support-media-compat-test-lib').projectDir = new File(rootDir, 'media-compat-test-lib')
+project(':support-media-compat-test-lib').projectDir = new File(rootDir, 'media-compat/version-compat-tests/lib')
/////////////////////////////
//
diff --git a/testutils/build.gradle b/testutils/build.gradle
index 15eabaf..6ecc012 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -19,6 +19,13 @@
}
dependencies {
+ api project(':support-fragment')
+ api project(':appcompat-v7')
+
+ compile libs.test_runner, { exclude module: 'support-annotations' }
+ compile libs.espresso_core, { exclude module: 'support-annotations' }
+ compile libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+ compile libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
compile libs.junit
}
diff --git a/testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java b/testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java
new file mode 100644
index 0000000..49ccc1b
--- /dev/null
+++ b/testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.testutils;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.support.test.rule.ActivityTestRule;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility methods for testing AppCompat activities.
+ */
+public class AppCompatActivityUtils {
+ private static final Runnable DO_NOTHING = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+
+ /**
+ * Waits for the execution of the provided activity test rule.
+ *
+ * @param rule Activity test rule to wait for.
+ */
+ public static void waitForExecution(
+ final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) {
+ // Wait for two cycles. When starting a postponed transition, it will post to
+ // the UI thread and then the execution will be added onto the queue after that.
+ // The two-cycle wait makes sure fragments have the opportunity to complete both
+ // before returning.
+ try {
+ rule.runOnUiThread(DO_NOTHING);
+ rule.runOnUiThread(DO_NOTHING);
+ } catch (Throwable throwable) {
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ private static void runOnUiThreadRethrow(
+ ActivityTestRule<? extends RecreatedAppCompatActivity> rule, Runnable r) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ r.run();
+ } else {
+ try {
+ rule.runOnUiThread(r);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+
+ /**
+ * Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed.
+ *
+ * @return The newly-restarted RecreatedAppCompatActivity
+ */
+ public static <T extends RecreatedAppCompatActivity> T recreateActivity(
+ ActivityTestRule<? extends RecreatedAppCompatActivity> rule, final T activity)
+ throws InterruptedException {
+ // Now switch the orientation
+ RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
+ RecreatedAppCompatActivity.sDestroyed = new CountDownLatch(1);
+
+ runOnUiThreadRethrow(rule, new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+ assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
+ assertTrue(RecreatedAppCompatActivity.sDestroyed.await(1, TimeUnit.SECONDS));
+ T newActivity = (T) RecreatedAppCompatActivity.sActivity;
+
+ waitForExecution(rule);
+
+ RecreatedAppCompatActivity.clearState();
+ return newActivity;
+ }
+}
diff --git a/testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java b/testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java
new file mode 100644
index 0000000..7d12deb
--- /dev/null
+++ b/testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.testutils;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.FragmentActivity;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility methods for testing fragment activities.
+ */
+public class FragmentActivityUtils {
+ private static final Runnable DO_NOTHING = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+
+ private static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) {
+ // Wait for two cycles. When starting a postponed transition, it will post to
+ // the UI thread and then the execution will be added onto the queue after that.
+ // The two-cycle wait makes sure fragments have the opportunity to complete both
+ // before returning.
+ try {
+ rule.runOnUiThread(DO_NOTHING);
+ rule.runOnUiThread(DO_NOTHING);
+ } catch (Throwable throwable) {
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ private static void runOnUiThreadRethrow(ActivityTestRule<? extends Activity> rule,
+ Runnable r) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ r.run();
+ } else {
+ try {
+ rule.runOnUiThread(r);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+
+ /**
+ * Restarts the RecreatedActivity and waits for the new activity to be resumed.
+ *
+ * @return The newly-restarted Activity
+ */
+ public static <T extends RecreatedActivity> T recreateActivity(
+ ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
+ throws InterruptedException {
+ // Now switch the orientation
+ RecreatedActivity.sResumed = new CountDownLatch(1);
+ RecreatedActivity.sDestroyed = new CountDownLatch(1);
+
+ runOnUiThreadRethrow(rule, new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+ assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
+ assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
+ T newActivity = (T) RecreatedActivity.sActivity;
+
+ waitForExecution(rule);
+
+ RecreatedActivity.clearState();
+ return newActivity;
+ }
+}
diff --git a/testutils/src/main/java/android/support/testutils/RecreatedActivity.java b/testutils/src/main/java/android/support/testutils/RecreatedActivity.java
new file mode 100644
index 0000000..aaea3a9
--- /dev/null
+++ b/testutils/src/main/java/android/support/testutils/RecreatedActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.testutils;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.FragmentActivity;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Extension of {@link FragmentActivity} that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * {@link FragmentActivityUtils#recreateActivity(ActivityTestRule, RecreatedActivity)} API.
+ */
+public class RecreatedActivity extends FragmentActivity {
+ // These must be cleared after each test using clearState()
+ public static RecreatedActivity sActivity;
+ public static CountDownLatch sResumed;
+ public static CountDownLatch sDestroyed;
+
+ static void clearState() {
+ sActivity = null;
+ sResumed = null;
+ sDestroyed = null;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sActivity = this;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (sResumed != null) {
+ sResumed.countDown();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (sDestroyed != null) {
+ sDestroyed.countDown();
+ }
+ }
+}
diff --git a/testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java b/testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java
new file mode 100644
index 0000000..d5645a3
--- /dev/null
+++ b/testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.testutils;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Extension of {@link AppCompatActivity} that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * {@link AppCompatActivityUtils#recreateActivity(ActivityTestRule, RecreatedAppCompatActivity)}
+ * API.
+ */
+public class RecreatedAppCompatActivity extends AppCompatActivity {
+ // These must be cleared after each test using clearState()
+ public static RecreatedAppCompatActivity sActivity;
+ public static CountDownLatch sResumed;
+ public static CountDownLatch sDestroyed;
+
+ static void clearState() {
+ sActivity = null;
+ sResumed = null;
+ sDestroyed = null;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sActivity = this;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (sResumed != null) {
+ sResumed.countDown();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (sDestroyed != null) {
+ sDestroyed.countDown();
+ }
+ }
+}
diff --git a/transition/Android.mk b/transition/Android.mk
index cbff183..8c76d6b 100644
--- a/transition/Android.mk
+++ b/transition/Android.mk
@@ -27,13 +27,7 @@
LOCAL_MODULE := android-support-transition
LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
LOCAL_SRC_FILES := \
- $(call all-java-files-under,base) \
- $(call all-java-files-under,api14) \
- $(call all-java-files-under,api18) \
- $(call all-java-files-under,api19) \
- $(call all-java-files-under,api21) \
- $(call all-java-files-under,api22) \
- $(call all-java-files-under,src)
+ $(call all-java-files-under,src/main/java)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_SHARED_ANDROID_LIBRARIES := \
android-support-annotations \
diff --git a/transition/build.gradle b/transition/build.gradle
index 326a681..cd2c237 100644
--- a/transition/build.gradle
+++ b/transition/build.gradle
@@ -24,15 +24,6 @@
}
sourceSets {
- main.java.srcDirs = [
- 'base',
- 'api14',
- 'api18',
- 'api19',
- 'api21',
- 'api22',
- 'src'
- ]
main.res.srcDirs = [
'res',
'res-public'
diff --git a/transition/src/android/support/transition/AutoTransition.java b/transition/src/android/support/transition/AutoTransition.java
deleted file mode 100644
index 02b49e2..0000000
--- a/transition/src/android/support/transition/AutoTransition.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.transition;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-/**
- * Utility class for creating a default transition that automatically fades,
- * moves, and resizes views during a scene change.
- *
- * <p>An AutoTransition can be described in a resource file by using the
- * tag <code>autoTransition</code>, along with the other standard
- * attributes of {@link Transition}.</p>
- */
-public class AutoTransition extends TransitionSet {
-
- /**
- * Constructs an AutoTransition object, which is a TransitionSet which
- * first fades out disappearing targets, then moves and resizes existing
- * targets, and finally fades in appearing targets.
- */
- public AutoTransition() {
- init();
- }
-
- public AutoTransition(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- private void init() {
- setOrdering(ORDERING_SEQUENTIAL);
- addTransition(new Fade(Fade.OUT)).
- addTransition(new ChangeBounds()).
- addTransition(new Fade(Fade.IN));
- }
-
-}
diff --git a/transition/src/android/support/transition/Transition.java b/transition/src/android/support/transition/Transition.java
deleted file mode 100644
index 04cc57b..0000000
--- a/transition/src/android/support/transition/Transition.java
+++ /dev/null
@@ -1,2437 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.transition;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.support.annotation.IdRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.v4.content.res.TypedArrayUtils;
-import android.support.v4.util.ArrayMap;
-import android.support.v4.util.LongSparseArray;
-import android.support.v4.view.ViewCompat;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.util.SparseIntArray;
-import android.view.InflateException;
-import android.view.SurfaceView;
-import android.view.TextureView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AnimationUtils;
-import android.widget.ListView;
-import android.widget.Spinner;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.StringTokenizer;
-
-/**
- * A Transition holds information about animations that will be run on its
- * targets during a scene change. Subclasses of this abstract class may
- * choreograph several child transitions ({@link TransitionSet} or they may
- * perform custom animations themselves. Any Transition has two main jobs:
- * (1) capture property values, and (2) play animations based on changes to
- * captured property values. A custom transition knows what property values
- * on View objects are of interest to it, and also knows how to animate
- * changes to those values. For example, the {@link Fade} transition tracks
- * changes to visibility-related properties and is able to construct and run
- * animations that fade items in or out based on changes to those properties.
- *
- * <p>Note: Transitions may not work correctly with either {@link SurfaceView}
- * or {@link TextureView}, due to the way that these views are displayed
- * on the screen. For SurfaceView, the problem is that the view is updated from
- * a non-UI thread, so changes to the view due to transitions (such as moving
- * and resizing the view) may be out of sync with the display inside those bounds.
- * TextureView is more compatible with transitions in general, but some
- * specific transitions (such as {@link Fade}) may not be compatible
- * with TextureView because they rely on {@link android.view.ViewOverlay}
- * functionality, which does not currently work with TextureView.</p>
- *
- * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code>
- * directory. Transition resources consist of a tag name for one of the Transition
- * subclasses along with attributes to define some of the attributes of that transition.
- * For example, here is a minimal resource file that declares a {@link ChangeBounds}
- * transition:</p>
- *
- * <pre>
- * <changeBounds/>
- * </pre>
- *
- * <p>Note that attributes for the transition are not required, just as they are
- * optional when declared in code; Transitions created from XML resources will use
- * the same defaults as their code-created equivalents. Here is a slightly more
- * elaborate example which declares a {@link TransitionSet} transition with
- * {@link ChangeBounds} and {@link Fade} child transitions:</p>
- *
- * <pre>
- * <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
- * android:transitionOrdering="sequential">
- * <changeBounds/>
- * <fade android:fadingMode="fade_out">
- * <targets>
- * <target android:targetId="@id/grayscaleContainer"/>
- * </targets>
- * </fade>
- * </transitionSet>
- * </pre>
- *
- * <p>In this example, the transitionOrdering attribute is used on the TransitionSet
- * object to change from the default {@link TransitionSet#ORDERING_TOGETHER} behavior
- * to be {@link TransitionSet#ORDERING_SEQUENTIAL} instead. Also, the {@link Fade}
- * transition uses a fadingMode of {@link Fade#OUT} instead of the default
- * out-in behavior. Finally, note the use of the <code>targets</code> sub-tag, which
- * takes a set of {code target} tags, each of which lists a specific <code>targetId</code> which
- * this transition acts upon. Use of targets is optional, but can be used to either limit the time
- * spent checking attributes on unchanging views, or limiting the types of animations run on
- * specific views. In this case, we know that only the <code>grayscaleContainer</code> will be
- * disappearing, so we choose to limit the {@link Fade} transition to only that view.</p>
- */
-public abstract class Transition implements Cloneable {
-
- private static final String LOG_TAG = "Transition";
- static final boolean DBG = false;
-
- /**
- * With {@link #setMatchOrder(int...)}, chooses to match by View instance.
- */
- public static final int MATCH_INSTANCE = 0x1;
- private static final int MATCH_FIRST = MATCH_INSTANCE;
-
- /**
- * With {@link #setMatchOrder(int...)}, chooses to match by
- * {@link android.view.View#getTransitionName()}. Null names will not be matched.
- */
- public static final int MATCH_NAME = 0x2;
-
- /**
- * With {@link #setMatchOrder(int...)}, chooses to match by
- * {@link android.view.View#getId()}. Negative IDs will not be matched.
- */
- public static final int MATCH_ID = 0x3;
-
- /**
- * With {@link #setMatchOrder(int...)}, chooses to match by the {@link android.widget.Adapter}
- * item id. When {@link android.widget.Adapter#hasStableIds()} returns false, no match
- * will be made for items.
- */
- public static final int MATCH_ITEM_ID = 0x4;
-
- private static final int MATCH_LAST = MATCH_ITEM_ID;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({MATCH_INSTANCE, MATCH_NAME, MATCH_ID, MATCH_ITEM_ID})
- @Retention(RetentionPolicy.SOURCE)
- public @interface MatchOrder {
- }
-
- private static final String MATCH_INSTANCE_STR = "instance";
- private static final String MATCH_NAME_STR = "name";
- private static final String MATCH_ID_STR = "id";
- private static final String MATCH_ITEM_ID_STR = "itemId";
-
- private static final int[] DEFAULT_MATCH_ORDER = {
- MATCH_NAME,
- MATCH_INSTANCE,
- MATCH_ID,
- MATCH_ITEM_ID,
- };
-
- private static final PathMotion STRAIGHT_PATH_MOTION = new PathMotion() {
- @Override
- public Path getPath(float startX, float startY, float endX, float endY) {
- Path path = new Path();
- path.moveTo(startX, startY);
- path.lineTo(endX, endY);
- return path;
- }
- };
-
- private String mName = getClass().getName();
-
- private long mStartDelay = -1;
- long mDuration = -1;
- private TimeInterpolator mInterpolator = null;
- ArrayList<Integer> mTargetIds = new ArrayList<>();
- ArrayList<View> mTargets = new ArrayList<>();
- private ArrayList<String> mTargetNames = null;
- private ArrayList<Class> mTargetTypes = null;
- private ArrayList<Integer> mTargetIdExcludes = null;
- private ArrayList<View> mTargetExcludes = null;
- private ArrayList<Class> mTargetTypeExcludes = null;
- private ArrayList<String> mTargetNameExcludes = null;
- private ArrayList<Integer> mTargetIdChildExcludes = null;
- private ArrayList<View> mTargetChildExcludes = null;
- private ArrayList<Class> mTargetTypeChildExcludes = null;
- private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
- private TransitionValuesMaps mEndValues = new TransitionValuesMaps();
- TransitionSet mParent = null;
- private int[] mMatchOrder = DEFAULT_MATCH_ORDER;
- private ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
- private ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts
-
- // Per-animator information used for later canceling when future transitions overlap
- private static ThreadLocal<ArrayMap<Animator, Transition.AnimationInfo>> sRunningAnimators =
- new ThreadLocal<>();
-
- // Scene Root is set at createAnimator() time in the cloned Transition
- private ViewGroup mSceneRoot = null;
-
- // Whether removing views from their parent is possible. This is only for views
- // in the start scene, which are no longer in the view hierarchy. This property
- // is determined by whether the previous Scene was created from a layout
- // resource, and thus the views from the exited scene are going away anyway
- // and can be removed as necessary to achieve a particular effect, such as
- // removing them from parents to add them to overlays.
- boolean mCanRemoveViews = false;
-
- // Track all animators in use in case the transition gets canceled and needs to
- // cancel running animators
- private ArrayList<Animator> mCurrentAnimators = new ArrayList<>();
-
- // Number of per-target instances of this Transition currently running. This count is
- // determined by calls to start() and end()
- private int mNumInstances = 0;
-
- // Whether this transition is currently paused, due to a call to pause()
- private boolean mPaused = false;
-
- // Whether this transition has ended. Used to avoid pause/resume on transitions
- // that have completed
- private boolean mEnded = false;
-
- // The set of listeners to be sent transition lifecycle events.
- private ArrayList<Transition.TransitionListener> mListeners = null;
-
- // The set of animators collected from calls to createAnimator(),
- // to be run in runAnimators()
- private ArrayList<Animator> mAnimators = new ArrayList<>();
-
- // The function for calculating the Animation start delay.
- TransitionPropagation mPropagation;
-
- // The rectangular region for Transitions like Explode and TransitionPropagations
- // like CircularPropagation
- private EpicenterCallback mEpicenterCallback;
-
- // For Fragment shared element transitions, linking views explicitly by mismatching
- // transitionNames.
- private ArrayMap<String, String> mNameOverrides;
-
- // The function used to interpolate along two-dimensional points. Typically used
- // for adding curves to x/y View motion.
- private PathMotion mPathMotion = STRAIGHT_PATH_MOTION;
-
- /**
- * Constructs a Transition object with no target objects. A transition with
- * no targets defaults to running on all target objects in the scene hierarchy
- * (if the transition is not contained in a TransitionSet), or all target
- * objects passed down from its parent (if it is in a TransitionSet).
- */
- public Transition() {
- }
-
- /**
- * Perform inflation from XML and apply a class-specific base style from a
- * theme attribute or style resource. This constructor of Transition allows
- * subclasses to use their own base style when they are inflating.
- *
- * @param context The Context the transition is running in, through which it can
- * access the current theme, resources, etc.
- * @param attrs The attributes of the XML tag that is inflating the transition.
- */
- public Transition(Context context, AttributeSet attrs) {
- TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION);
- XmlResourceParser parser = (XmlResourceParser) attrs;
- long duration = TypedArrayUtils.getNamedInt(a, parser, "duration",
- Styleable.Transition.DURATION, -1);
- if (duration >= 0) {
- setDuration(duration);
- }
- long startDelay = TypedArrayUtils.getNamedInt(a, parser, "startDelay",
- Styleable.Transition.START_DELAY, -1);
- if (startDelay > 0) {
- setStartDelay(startDelay);
- }
- final int resId = TypedArrayUtils.getNamedResourceId(a, parser, "interpolator",
- Styleable.Transition.INTERPOLATOR, 0);
- if (resId > 0) {
- setInterpolator(AnimationUtils.loadInterpolator(context, resId));
- }
- String matchOrder = TypedArrayUtils.getNamedString(a, parser, "matchOrder",
- Styleable.Transition.MATCH_ORDER);
- if (matchOrder != null) {
- setMatchOrder(parseMatchOrder(matchOrder));
- }
- a.recycle();
- }
-
- @MatchOrder
- private static int[] parseMatchOrder(String matchOrderString) {
- StringTokenizer st = new StringTokenizer(matchOrderString, ",");
- @MatchOrder
- int[] matches = new int[st.countTokens()];
- int index = 0;
- while (st.hasMoreTokens()) {
- String token = st.nextToken().trim();
- if (MATCH_ID_STR.equalsIgnoreCase(token)) {
- matches[index] = Transition.MATCH_ID;
- } else if (MATCH_INSTANCE_STR.equalsIgnoreCase(token)) {
- matches[index] = Transition.MATCH_INSTANCE;
- } else if (MATCH_NAME_STR.equalsIgnoreCase(token)) {
- matches[index] = Transition.MATCH_NAME;
- } else if (MATCH_ITEM_ID_STR.equalsIgnoreCase(token)) {
- matches[index] = Transition.MATCH_ITEM_ID;
- } else if (token.isEmpty()) {
- @MatchOrder
- int[] smallerMatches = new int[matches.length - 1];
- System.arraycopy(matches, 0, smallerMatches, 0, index);
- matches = smallerMatches;
- index--;
- } else {
- throw new InflateException("Unknown match type in matchOrder: '" + token + "'");
- }
- index++;
- }
- return matches;
- }
-
- /**
- * Sets the duration of this transition. By default, there is no duration
- * (indicated by a negative number), which means that the Animator created by
- * the transition will have its own specified duration. If the duration of a
- * Transition is set, that duration will override the Animator duration.
- *
- * @param duration The length of the animation, in milliseconds.
- * @return This transition object.
- */
- @NonNull
- public Transition setDuration(long duration) {
- mDuration = duration;
- return this;
- }
-
- /**
- * Returns the duration set on this transition. If no duration has been set,
- * the returned value will be negative, indicating that resulting animators will
- * retain their own durations.
- *
- * @return The duration set on this transition, in milliseconds, if one has been
- * set, otherwise returns a negative number.
- */
- public long getDuration() {
- return mDuration;
- }
-
- /**
- * Sets the startDelay of this transition. By default, there is no delay
- * (indicated by a negative number), which means that the Animator created by
- * the transition will have its own specified startDelay. If the delay of a
- * Transition is set, that delay will override the Animator delay.
- *
- * @param startDelay The length of the delay, in milliseconds.
- * @return This transition object.
- */
- @NonNull
- public Transition setStartDelay(long startDelay) {
- mStartDelay = startDelay;
- return this;
- }
-
- /**
- * Returns the startDelay set on this transition. If no startDelay has been set,
- * the returned value will be negative, indicating that resulting animators will
- * retain their own startDelays.
- *
- * @return The startDelay set on this transition, in milliseconds, if one has
- * been set, otherwise returns a negative number.
- */
- public long getStartDelay() {
- return mStartDelay;
- }
-
- /**
- * Sets the interpolator of this transition. By default, the interpolator
- * is null, which means that the Animator created by the transition
- * will have its own specified interpolator. If the interpolator of a
- * Transition is set, that interpolator will override the Animator interpolator.
- *
- * @param interpolator The time interpolator used by the transition
- * @return This transition object.
- */
- @NonNull
- public Transition setInterpolator(@Nullable TimeInterpolator interpolator) {
- mInterpolator = interpolator;
- return this;
- }
-
- /**
- * Returns the interpolator set on this transition. If no interpolator has been set,
- * the returned value will be null, indicating that resulting animators will
- * retain their own interpolators.
- *
- * @return The interpolator set on this transition, if one has been set, otherwise
- * returns null.
- */
- @Nullable
- public TimeInterpolator getInterpolator() {
- return mInterpolator;
- }
-
- /**
- * Returns the set of property names used stored in the {@link TransitionValues}
- * object passed into {@link #captureStartValues(TransitionValues)} that
- * this transition cares about for the purposes of canceling overlapping animations.
- * When any transition is started on a given scene root, all transitions
- * currently running on that same scene root are checked to see whether the
- * properties on which they based their animations agree with the end values of
- * the same properties in the new transition. If the end values are not equal,
- * then the old animation is canceled since the new transition will start a new
- * animation to these new values. If the values are equal, the old animation is
- * allowed to continue and no new animation is started for that transition.
- *
- * <p>A transition does not need to override this method. However, not doing so
- * will mean that the cancellation logic outlined in the previous paragraph
- * will be skipped for that transition, possibly leading to artifacts as
- * old transitions and new transitions on the same targets run in parallel,
- * animating views toward potentially different end values.</p>
- *
- * @return An array of property names as described in the class documentation for
- * {@link TransitionValues}. The default implementation returns <code>null</code>.
- */
- @Nullable
- public String[] getTransitionProperties() {
- return null;
- }
-
- /**
- * This method creates an animation that will be run for this transition
- * given the information in the startValues and endValues structures captured
- * earlier for the start and end scenes. Subclasses of Transition should override
- * this method. The method should only be called by the transition system; it is
- * not intended to be called from external classes.
- *
- * <p>This method is called by the transition's parent (all the way up to the
- * topmost Transition in the hierarchy) with the sceneRoot and start/end
- * values that the transition may need to set up initial target values
- * and construct an appropriate animation. For example, if an overall
- * Transition is a {@link TransitionSet} consisting of several
- * child transitions in sequence, then some of the child transitions may
- * want to set initial values on target views prior to the overall
- * Transition commencing, to put them in an appropriate state for the
- * delay between that start and the child Transition start time. For
- * example, a transition that fades an item in may wish to set the starting
- * alpha value to 0, to avoid it blinking in prior to the transition
- * actually starting the animation. This is necessary because the scene
- * change that triggers the Transition will automatically set the end-scene
- * on all target views, so a Transition that wants to animate from a
- * different value should set that value prior to returning from this method.</p>
- *
- * <p>Additionally, a Transition can perform logic to determine whether
- * the transition needs to run on the given target and start/end values.
- * For example, a transition that resizes objects on the screen may wish
- * to avoid running for views which are not present in either the start
- * or end scenes.</p>
- *
- * <p>If there is an animator created and returned from this method, the
- * transition mechanism will apply any applicable duration, startDelay,
- * and interpolator to that animation and start it. A return value of
- * <code>null</code> indicates that no animation should run. The default
- * implementation returns null.</p>
- *
- * <p>The method is called for every applicable target object, which is
- * stored in the {@link TransitionValues#view} field.</p>
- *
- * @param sceneRoot The root of the transition hierarchy.
- * @param startValues The values for a specific target in the start scene.
- * @param endValues The values for the target in the end scene.
- * @return A Animator to be started at the appropriate time in the
- * overall transition for this scene change. A null value means no animation
- * should be run.
- */
- @Nullable
- public Animator createAnimator(@NonNull ViewGroup sceneRoot,
- @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
- return null;
- }
-
- /**
- * Sets the order in which Transition matches View start and end values.
- * <p>
- * The default behavior is to match first by {@link android.view.View#getTransitionName()},
- * then by View instance, then by {@link android.view.View#getId()} and finally
- * by its item ID if it is in a direct child of ListView. The caller can
- * choose to have only some or all of the values of {@link #MATCH_INSTANCE},
- * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. Only
- * the match algorithms supplied will be used to determine whether Views are the
- * the same in both the start and end Scene. Views that do not match will be considered
- * as entering or leaving the Scene.
- * </p>
- *
- * @param matches A list of zero or more of {@link #MATCH_INSTANCE},
- * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}.
- * If none are provided, then the default match order will be set.
- */
- public void setMatchOrder(@MatchOrder int... matches) {
- if (matches == null || matches.length == 0) {
- mMatchOrder = DEFAULT_MATCH_ORDER;
- } else {
- for (int i = 0; i < matches.length; i++) {
- int match = matches[i];
- if (!isValidMatch(match)) {
- throw new IllegalArgumentException("matches contains invalid value");
- }
- if (alreadyContains(matches, i)) {
- throw new IllegalArgumentException("matches contains a duplicate value");
- }
- }
- mMatchOrder = matches.clone();
- }
- }
-
- private static boolean isValidMatch(int match) {
- return (match >= MATCH_FIRST && match <= MATCH_LAST);
- }
-
- private static boolean alreadyContains(int[] array, int searchIndex) {
- int value = array[searchIndex];
- for (int i = 0; i < searchIndex; i++) {
- if (array[i] == value) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Match start/end values by View instance. Adds matched values to mStartValuesList
- * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd.
- */
- private void matchInstances(ArrayMap<View, TransitionValues> unmatchedStart,
- ArrayMap<View, TransitionValues> unmatchedEnd) {
- for (int i = unmatchedStart.size() - 1; i >= 0; i--) {
- View view = unmatchedStart.keyAt(i);
- if (view != null && isValidTarget(view)) {
- TransitionValues end = unmatchedEnd.remove(view);
- if (end != null && end.view != null && isValidTarget(end.view)) {
- TransitionValues start = unmatchedStart.removeAt(i);
- mStartValuesList.add(start);
- mEndValuesList.add(end);
- }
- }
- }
- }
-
- /**
- * Match start/end values by Adapter item ID. Adds matched values to mStartValuesList
- * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
- * startItemIds and endItemIds as a guide for which Views have unique item IDs.
- */
- private void matchItemIds(ArrayMap<View, TransitionValues> unmatchedStart,
- ArrayMap<View, TransitionValues> unmatchedEnd,
- LongSparseArray<View> startItemIds, LongSparseArray<View> endItemIds) {
- int numStartIds = startItemIds.size();
- for (int i = 0; i < numStartIds; i++) {
- View startView = startItemIds.valueAt(i);
- if (startView != null && isValidTarget(startView)) {
- View endView = endItemIds.get(startItemIds.keyAt(i));
- if (endView != null && isValidTarget(endView)) {
- TransitionValues startValues = unmatchedStart.get(startView);
- TransitionValues endValues = unmatchedEnd.get(endView);
- if (startValues != null && endValues != null) {
- mStartValuesList.add(startValues);
- mEndValuesList.add(endValues);
- unmatchedStart.remove(startView);
- unmatchedEnd.remove(endView);
- }
- }
- }
- }
- }
-
- /**
- * Match start/end values by Adapter view ID. Adds matched values to mStartValuesList
- * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
- * startIds and endIds as a guide for which Views have unique IDs.
- */
- private void matchIds(ArrayMap<View, TransitionValues> unmatchedStart,
- ArrayMap<View, TransitionValues> unmatchedEnd,
- SparseArray<View> startIds, SparseArray<View> endIds) {
- int numStartIds = startIds.size();
- for (int i = 0; i < numStartIds; i++) {
- View startView = startIds.valueAt(i);
- if (startView != null && isValidTarget(startView)) {
- View endView = endIds.get(startIds.keyAt(i));
- if (endView != null && isValidTarget(endView)) {
- TransitionValues startValues = unmatchedStart.get(startView);
- TransitionValues endValues = unmatchedEnd.get(endView);
- if (startValues != null && endValues != null) {
- mStartValuesList.add(startValues);
- mEndValuesList.add(endValues);
- unmatchedStart.remove(startView);
- unmatchedEnd.remove(endView);
- }
- }
- }
- }
- }
-
- /**
- * Match start/end values by Adapter transitionName. Adds matched values to mStartValuesList
- * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
- * startNames and endNames as a guide for which Views have unique transitionNames.
- */
- private void matchNames(ArrayMap<View, TransitionValues> unmatchedStart,
- ArrayMap<View, TransitionValues> unmatchedEnd,
- ArrayMap<String, View> startNames, ArrayMap<String, View> endNames) {
- int numStartNames = startNames.size();
- for (int i = 0; i < numStartNames; i++) {
- View startView = startNames.valueAt(i);
- if (startView != null && isValidTarget(startView)) {
- View endView = endNames.get(startNames.keyAt(i));
- if (endView != null && isValidTarget(endView)) {
- TransitionValues startValues = unmatchedStart.get(startView);
- TransitionValues endValues = unmatchedEnd.get(endView);
- if (startValues != null && endValues != null) {
- mStartValuesList.add(startValues);
- mEndValuesList.add(endValues);
- unmatchedStart.remove(startView);
- unmatchedEnd.remove(endView);
- }
- }
- }
- }
- }
-
- /**
- * Adds all values from unmatchedStart and unmatchedEnd to mStartValuesList and mEndValuesList,
- * assuming that there is no match between values in the list.
- */
- private void addUnmatched(ArrayMap<View, TransitionValues> unmatchedStart,
- ArrayMap<View, TransitionValues> unmatchedEnd) {
- // Views that only exist in the start Scene
- for (int i = 0; i < unmatchedStart.size(); i++) {
- final TransitionValues start = unmatchedStart.valueAt(i);
- if (isValidTarget(start.view)) {
- mStartValuesList.add(start);
- mEndValuesList.add(null);
- }
- }
-
- // Views that only exist in the end Scene
- for (int i = 0; i < unmatchedEnd.size(); i++) {
- final TransitionValues end = unmatchedEnd.valueAt(i);
- if (isValidTarget(end.view)) {
- mEndValuesList.add(end);
- mStartValuesList.add(null);
- }
- }
- }
-
- private void matchStartAndEnd(TransitionValuesMaps startValues,
- TransitionValuesMaps endValues) {
- ArrayMap<View, TransitionValues> unmatchedStart = new ArrayMap<>(startValues.mViewValues);
- ArrayMap<View, TransitionValues> unmatchedEnd = new ArrayMap<>(endValues.mViewValues);
-
- for (int i = 0; i < mMatchOrder.length; i++) {
- switch (mMatchOrder[i]) {
- case MATCH_INSTANCE:
- matchInstances(unmatchedStart, unmatchedEnd);
- break;
- case MATCH_NAME:
- matchNames(unmatchedStart, unmatchedEnd,
- startValues.mNameValues, endValues.mNameValues);
- break;
- case MATCH_ID:
- matchIds(unmatchedStart, unmatchedEnd,
- startValues.mIdValues, endValues.mIdValues);
- break;
- case MATCH_ITEM_ID:
- matchItemIds(unmatchedStart, unmatchedEnd,
- startValues.mItemIdValues, endValues.mItemIdValues);
- break;
- }
- }
- addUnmatched(unmatchedStart, unmatchedEnd);
- }
-
- /**
- * This method, essentially a wrapper around all calls to createAnimator for all
- * possible target views, is called with the entire set of start/end
- * values. The implementation in Transition iterates through these lists
- * and calls {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
- * with each set of start/end values on this transition. The
- * TransitionSet subclass overrides this method and delegates it to
- * each of its children in succession.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
- TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
- ArrayList<TransitionValues> endValuesList) {
- if (DBG) {
- Log.d(LOG_TAG, "createAnimators() for " + this);
- }
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- long minStartDelay = Long.MAX_VALUE;
- SparseIntArray startDelays = new SparseIntArray();
- int startValuesListCount = startValuesList.size();
- for (int i = 0; i < startValuesListCount; ++i) {
- TransitionValues start = startValuesList.get(i);
- TransitionValues end = endValuesList.get(i);
- if (start != null && !start.mTargetedTransitions.contains(this)) {
- start = null;
- }
- if (end != null && !end.mTargetedTransitions.contains(this)) {
- end = null;
- }
- if (start == null && end == null) {
- continue;
- }
- // Only bother trying to animate with values that differ between start/end
- boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
- if (isChanged) {
- if (DBG) {
- View view = (end != null) ? end.view : start.view;
- Log.d(LOG_TAG, " differing start/end values for view " + view);
- if (start == null || end == null) {
- Log.d(LOG_TAG, " " + ((start == null)
- ? "start null, end non-null" : "start non-null, end null"));
- } else {
- for (String key : start.values.keySet()) {
- Object startValue = start.values.get(key);
- Object endValue = end.values.get(key);
- if (startValue != endValue && !startValue.equals(endValue)) {
- Log.d(LOG_TAG, " " + key + ": start(" + startValue
- + "), end(" + endValue + ")");
- }
- }
- }
- }
- // TODO: what to do about targetIds and itemIds?
- Animator animator = createAnimator(sceneRoot, start, end);
- if (animator != null) {
- // Save animation info for future cancellation purposes
- View view;
- TransitionValues infoValues = null;
- if (end != null) {
- view = end.view;
- String[] properties = getTransitionProperties();
- if (view != null && properties != null && properties.length > 0) {
- infoValues = new TransitionValues();
- infoValues.view = view;
- TransitionValues newValues = endValues.mViewValues.get(view);
- if (newValues != null) {
- for (int j = 0; j < properties.length; ++j) {
- infoValues.values.put(properties[j],
- newValues.values.get(properties[j]));
- }
- }
- int numExistingAnims = runningAnimators.size();
- for (int j = 0; j < numExistingAnims; ++j) {
- Animator anim = runningAnimators.keyAt(j);
- AnimationInfo info = runningAnimators.get(anim);
- if (info.mValues != null && info.mView == view
- && info.mName.equals(getName())) {
- if (info.mValues.equals(infoValues)) {
- // Favor the old animator
- animator = null;
- break;
- }
- }
- }
- }
- } else {
- view = start.view;
- }
- if (animator != null) {
- if (mPropagation != null) {
- long delay = mPropagation.getStartDelay(sceneRoot, this, start, end);
- startDelays.put(mAnimators.size(), (int) delay);
- minStartDelay = Math.min(delay, minStartDelay);
- }
- AnimationInfo info = new AnimationInfo(view, getName(), this,
- ViewUtils.getWindowId(sceneRoot), infoValues);
- runningAnimators.put(animator, info);
- mAnimators.add(animator);
- }
- }
- }
- }
- if (minStartDelay != 0) {
- for (int i = 0; i < startDelays.size(); i++) {
- int index = startDelays.keyAt(i);
- Animator animator = mAnimators.get(index);
- long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
- animator.setStartDelay(delay);
- }
- }
- }
-
- /**
- * Internal utility method for checking whether a given view/id
- * is valid for this transition, where "valid" means that either
- * the Transition has no target/targetId list (the default, in which
- * cause the transition should act on all views in the hiearchy), or
- * the given view is in the target list or the view id is in the
- * targetId list. If the target parameter is null, then the target list
- * is not checked (this is in the case of ListView items, where the
- * views are ignored and only the ids are used).
- */
- boolean isValidTarget(View target) {
- int targetId = target.getId();
- if (mTargetIdExcludes != null && mTargetIdExcludes.contains(targetId)) {
- return false;
- }
- if (mTargetExcludes != null && mTargetExcludes.contains(target)) {
- return false;
- }
- if (mTargetTypeExcludes != null) {
- int numTypes = mTargetTypeExcludes.size();
- for (int i = 0; i < numTypes; ++i) {
- Class type = mTargetTypeExcludes.get(i);
- if (type.isInstance(target)) {
- return false;
- }
- }
- }
- if (mTargetNameExcludes != null && ViewCompat.getTransitionName(target) != null) {
- if (mTargetNameExcludes.contains(ViewCompat.getTransitionName(target))) {
- return false;
- }
- }
- if (mTargetIds.size() == 0 && mTargets.size() == 0
- && (mTargetTypes == null || mTargetTypes.isEmpty())
- && (mTargetNames == null || mTargetNames.isEmpty())) {
- return true;
- }
- if (mTargetIds.contains(targetId) || mTargets.contains(target)) {
- return true;
- }
- if (mTargetNames != null && mTargetNames.contains(ViewCompat.getTransitionName(target))) {
- return true;
- }
- if (mTargetTypes != null) {
- for (int i = 0; i < mTargetTypes.size(); ++i) {
- if (mTargetTypes.get(i).isInstance(target)) {
- return true;
- }
- }
- }
- return false;
- }
-
- private static ArrayMap<Animator, AnimationInfo> getRunningAnimators() {
- ArrayMap<Animator, AnimationInfo> runningAnimators = sRunningAnimators.get();
- if (runningAnimators == null) {
- runningAnimators = new ArrayMap<>();
- sRunningAnimators.set(runningAnimators);
- }
- return runningAnimators;
- }
-
- /**
- * This is called internally once all animations have been set up by the
- * transition hierarchy. \
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void runAnimators() {
- if (DBG) {
- Log.d(LOG_TAG, "runAnimators() on " + this);
- }
- start();
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- // Now start every Animator that was previously created for this transition
- for (Animator anim : mAnimators) {
- if (DBG) {
- Log.d(LOG_TAG, " anim: " + anim);
- }
- if (runningAnimators.containsKey(anim)) {
- start();
- runAnimator(anim, runningAnimators);
- }
- }
- mAnimators.clear();
- end();
- }
-
- private void runAnimator(Animator animator,
- final ArrayMap<Animator, AnimationInfo> runningAnimators) {
- if (animator != null) {
- // TODO: could be a single listener instance for all of them since it uses the param
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mCurrentAnimators.add(animation);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- runningAnimators.remove(animation);
- mCurrentAnimators.remove(animation);
- }
- });
- animate(animator);
- }
- }
-
- /**
- * Captures the values in the start scene for the properties that this
- * transition monitors. These values are then passed as the startValues
- * structure in a later call to
- * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
- * The main concern for an implementation is what the
- * properties are that the transition cares about and what the values are
- * for all of those properties. The start and end values will be compared
- * later during the
- * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
- * method to determine what, if any, animations, should be run.
- *
- * <p>Subclasses must implement this method. The method should only be called by the
- * transition system; it is not intended to be called from external classes.</p>
- *
- * @param transitionValues The holder for any values that the Transition
- * wishes to store. Values are stored in the <code>values</code> field
- * of this TransitionValues object and are keyed from
- * a String value. For example, to store a view's rotation value,
- * a transition might call
- * <code>transitionValues.values.put("appname:transitionname:rotation",
- * view.getRotation())</code>. The target view will already be stored
- * in
- * the transitionValues structure when this method is called.
- * @see #captureEndValues(TransitionValues)
- * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
- */
- public abstract void captureStartValues(@NonNull TransitionValues transitionValues);
-
- /**
- * Captures the values in the end scene for the properties that this
- * transition monitors. These values are then passed as the endValues
- * structure in a later call to
- * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
- * The main concern for an implementation is what the
- * properties are that the transition cares about and what the values are
- * for all of those properties. The start and end values will be compared
- * later during the
- * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
- * method to determine what, if any, animations, should be run.
- *
- * <p>Subclasses must implement this method. The method should only be called by the
- * transition system; it is not intended to be called from external classes.</p>
- *
- * @param transitionValues The holder for any values that the Transition
- * wishes to store. Values are stored in the <code>values</code> field
- * of this TransitionValues object and are keyed from
- * a String value. For example, to store a view's rotation value,
- * a transition might call
- * <code>transitionValues.values.put("appname:transitionname:rotation",
- * view.getRotation())</code>. The target view will already be stored
- * in
- * the transitionValues structure when this method is called.
- * @see #captureStartValues(TransitionValues)
- * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
- */
- public abstract void captureEndValues(@NonNull TransitionValues transitionValues);
-
- /**
- * Sets the target view instances that this Transition is interested in
- * animating. By default, there are no targets, and a Transition will
- * listen for changes on every view in the hierarchy below the sceneRoot
- * of the Scene being transitioned into. Setting targets constrains
- * the Transition to only listen for, and act on, these views.
- * All other views will be ignored.
- *
- * <p>The target list is like the {@link #addTarget(int) targetId}
- * list except this list specifies the actual View instances, not the ids
- * of the views. This is an important distinction when scene changes involve
- * view hierarchies which have been inflated separately; different views may
- * share the same id but not actually be the same instance. If the transition
- * should treat those views as the same, then {@link #addTarget(int)} should be used
- * instead of {@link #addTarget(View)}. If, on the other hand, scene changes involve
- * changes all within the same view hierarchy, among views which do not
- * necessarily have ids set on them, then the target list of views may be more
- * convenient.</p>
- *
- * @param target A View on which the Transition will act, must be non-null.
- * @return The Transition to which the target is added.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).addTarget(someView);</code>
- * @see #addTarget(int)
- */
- @NonNull
- public Transition addTarget(@NonNull View target) {
- mTargets.add(target);
- return this;
- }
-
- /**
- * Adds the id of a target view that this Transition is interested in
- * animating. By default, there are no targetIds, and a Transition will
- * listen for changes on every view in the hierarchy below the sceneRoot
- * of the Scene being transitioned into. Setting targetIds constrains
- * the Transition to only listen for, and act on, views with these IDs.
- * Views with different IDs, or no IDs whatsoever, will be ignored.
- *
- * <p>Note that using ids to specify targets implies that ids should be unique
- * within the view hierarchy underneath the scene root.</p>
- *
- * @param targetId The id of a target view, must be a positive number.
- * @return The Transition to which the targetId is added.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).addTarget(someId);</code>
- * @see View#getId()
- */
- @NonNull
- public Transition addTarget(@IdRes int targetId) {
- if (targetId > 0) {
- mTargetIds.add(targetId);
- }
- return this;
- }
-
- /**
- * Adds the transitionName of a target view that this Transition is interested in
- * animating. By default, there are no targetNames, and a Transition will
- * listen for changes on every view in the hierarchy below the sceneRoot
- * of the Scene being transitioned into. Setting targetNames constrains
- * the Transition to only listen for, and act on, views with these transitionNames.
- * Views with different transitionNames, or no transitionName whatsoever, will be ignored.
- *
- * <p>Note that transitionNames should be unique within the view hierarchy.</p>
- *
- * @param targetName The transitionName of a target view, must be non-null.
- * @return The Transition to which the target transitionName is added.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).addTarget(someName);</code>
- * @see ViewCompat#getTransitionName(View)
- */
- @NonNull
- public Transition addTarget(@NonNull String targetName) {
- if (mTargetNames == null) {
- mTargetNames = new ArrayList<>();
- }
- mTargetNames.add(targetName);
- return this;
- }
-
- /**
- * Adds the Class of a target view that this Transition is interested in
- * animating. By default, there are no targetTypes, and a Transition will
- * listen for changes on every view in the hierarchy below the sceneRoot
- * of the Scene being transitioned into. Setting targetTypes constrains
- * the Transition to only listen for, and act on, views with these classes.
- * Views with different classes will be ignored.
- *
- * <p>Note that any View that can be cast to targetType will be included, so
- * if targetType is <code>View.class</code>, all Views will be included.</p>
- *
- * @param targetType The type to include when running this transition.
- * @return The Transition to which the target class was added.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);</code>
- * @see #addTarget(int)
- * @see #addTarget(android.view.View)
- * @see #excludeTarget(Class, boolean)
- * @see #excludeChildren(Class, boolean)
- */
- @NonNull
- public Transition addTarget(@NonNull Class targetType) {
- if (mTargetTypes == null) {
- mTargetTypes = new ArrayList<>();
- }
- mTargetTypes.add(targetType);
- return this;
- }
-
- /**
- * Removes the given target from the list of targets that this Transition
- * is interested in animating.
- *
- * @param target The target view, must be non-null.
- * @return Transition The Transition from which the target is removed.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).removeTarget(someView);</code>
- */
- @NonNull
- public Transition removeTarget(@NonNull View target) {
- mTargets.remove(target);
- return this;
- }
-
- /**
- * Removes the given targetId from the list of ids that this Transition
- * is interested in animating.
- *
- * @param targetId The id of a target view, must be a positive number.
- * @return The Transition from which the targetId is removed.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).removeTargetId(someId);</code>
- */
- @NonNull
- public Transition removeTarget(@IdRes int targetId) {
- if (targetId > 0) {
- mTargetIds.remove((Integer) targetId);
- }
- return this;
- }
-
- /**
- * Removes the given targetName from the list of transitionNames that this Transition
- * is interested in animating.
- *
- * @param targetName The transitionName of a target view, must not be null.
- * @return The Transition from which the targetName is removed.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).removeTargetName(someName);</code>
- */
- @NonNull
- public Transition removeTarget(@NonNull String targetName) {
- if (mTargetNames != null) {
- mTargetNames.remove(targetName);
- }
- return this;
- }
-
- /**
- * Removes the given target from the list of targets that this Transition
- * is interested in animating.
- *
- * @param target The type of the target view, must be non-null.
- * @return Transition The Transition from which the target is removed.
- * Returning the same object makes it easier to chain calls during
- * construction, such as
- * <code>transitionSet.addTransitions(new Fade()).removeTarget(someType);</code>
- */
- @NonNull
- public Transition removeTarget(@NonNull Class target) {
- if (mTargetTypes != null) {
- mTargetTypes.remove(target);
- }
- return this;
- }
-
- /**
- * Utility method to manage the boilerplate code that is the same whether we
- * are excluding targets or their children.
- */
- private static <T> ArrayList<T> excludeObject(ArrayList<T> list, T target, boolean exclude) {
- if (target != null) {
- if (exclude) {
- list = ArrayListManager.add(list, target);
- } else {
- list = ArrayListManager.remove(list, target);
- }
- }
- return list;
- }
-
- /**
- * Whether to add the given target to the list of targets to exclude from this
- * transition. The <code>exclude</code> parameter specifies whether the target
- * should be added to or removed from the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param target The target to ignore when running this transition.
- * @param exclude Whether to add the target to or remove the target from the
- * current list of excluded targets.
- * @return This transition object.
- * @see #excludeChildren(View, boolean)
- * @see #excludeTarget(int, boolean)
- * @see #excludeTarget(Class, boolean)
- */
- @NonNull
- public Transition excludeTarget(@NonNull View target, boolean exclude) {
- mTargetExcludes = excludeView(mTargetExcludes, target, exclude);
- return this;
- }
-
- /**
- * Whether to add the given id to the list of target ids to exclude from this
- * transition. The <code>exclude</code> parameter specifies whether the target
- * should be added to or removed from the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param targetId The id of a target to ignore when running this transition.
- * @param exclude Whether to add the target to or remove the target from the
- * current list of excluded targets.
- * @return This transition object.
- * @see #excludeChildren(int, boolean)
- * @see #excludeTarget(View, boolean)
- * @see #excludeTarget(Class, boolean)
- */
- @NonNull
- public Transition excludeTarget(@IdRes int targetId, boolean exclude) {
- mTargetIdExcludes = excludeId(mTargetIdExcludes, targetId, exclude);
- return this;
- }
-
- /**
- * Whether to add the given transitionName to the list of target transitionNames to exclude
- * from this transition. The <code>exclude</code> parameter specifies whether the target
- * should be added to or removed from the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded by their
- * id, their instance reference, their transitionName, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param targetName The name of a target to ignore when running this transition.
- * @param exclude Whether to add the target to or remove the target from the
- * current list of excluded targets.
- * @return This transition object.
- * @see #excludeTarget(View, boolean)
- * @see #excludeTarget(int, boolean)
- * @see #excludeTarget(Class, boolean)
- */
- @NonNull
- public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
- mTargetNameExcludes = excludeObject(mTargetNameExcludes, targetName, exclude);
- return this;
- }
-
- /**
- * Whether to add the children of given target to the list of target children
- * to exclude from this transition. The <code>exclude</code> parameter specifies
- * whether the target should be added to or removed from the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param target The target to ignore when running this transition.
- * @param exclude Whether to add the target to or remove the target from the
- * current list of excluded targets.
- * @return This transition object.
- * @see #excludeTarget(View, boolean)
- * @see #excludeChildren(int, boolean)
- * @see #excludeChildren(Class, boolean)
- */
- @NonNull
- public Transition excludeChildren(@NonNull View target, boolean exclude) {
- mTargetChildExcludes = excludeView(mTargetChildExcludes, target, exclude);
- return this;
- }
-
- /**
- * Whether to add the children of the given id to the list of targets to exclude
- * from this transition. The <code>exclude</code> parameter specifies whether
- * the children of the target should be added to or removed from the excluded list.
- * Excluding children in this way provides a simple mechanism for excluding all
- * children of specific targets, rather than individually excluding each
- * child individually.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param targetId The id of a target whose children should be ignored when running
- * this transition.
- * @param exclude Whether to add the target to or remove the target from the
- * current list of excluded-child targets.
- * @return This transition object.
- * @see #excludeTarget(int, boolean)
- * @see #excludeChildren(View, boolean)
- * @see #excludeChildren(Class, boolean)
- */
- @NonNull
- public Transition excludeChildren(@IdRes int targetId, boolean exclude) {
- mTargetIdChildExcludes = excludeId(mTargetIdChildExcludes, targetId, exclude);
- return this;
- }
-
- /**
- * Utility method to manage the boilerplate code that is the same whether we
- * are excluding targets or their children.
- */
- private ArrayList<Integer> excludeId(ArrayList<Integer> list, int targetId, boolean exclude) {
- if (targetId > 0) {
- if (exclude) {
- list = ArrayListManager.add(list, targetId);
- } else {
- list = ArrayListManager.remove(list, targetId);
- }
- }
- return list;
- }
-
- /**
- * Utility method to manage the boilerplate code that is the same whether we
- * are excluding targets or their children.
- */
- private ArrayList<View> excludeView(ArrayList<View> list, View target, boolean exclude) {
- if (target != null) {
- if (exclude) {
- list = ArrayListManager.add(list, target);
- } else {
- list = ArrayListManager.remove(list, target);
- }
- }
- return list;
- }
-
- /**
- * Whether to add the given type to the list of types to exclude from this
- * transition. The <code>exclude</code> parameter specifies whether the target
- * type should be added to or removed from the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param type The type to ignore when running this transition.
- * @param exclude Whether to add the target type to or remove it from the
- * current list of excluded target types.
- * @return This transition object.
- * @see #excludeChildren(Class, boolean)
- * @see #excludeTarget(int, boolean)
- * @see #excludeTarget(View, boolean)
- */
- @NonNull
- public Transition excludeTarget(@NonNull Class type, boolean exclude) {
- mTargetTypeExcludes = excludeType(mTargetTypeExcludes, type, exclude);
- return this;
- }
-
- /**
- * Whether to add the given type to the list of types whose children should
- * be excluded from this transition. The <code>exclude</code> parameter
- * specifies whether the target type should be added to or removed from
- * the excluded list.
- *
- * <p>Excluding targets is a general mechanism for allowing transitions to run on
- * a view hierarchy while skipping target views that should not be part of
- * the transition. For example, you may want to avoid animating children
- * of a specific ListView or Spinner. Views can be excluded either by their
- * id, or by their instance reference, or by the Class of that view
- * (eg, {@link Spinner}).</p>
- *
- * @param type The type to ignore when running this transition.
- * @param exclude Whether to add the target type to or remove it from the
- * current list of excluded target types.
- * @return This transition object.
- * @see #excludeTarget(Class, boolean)
- * @see #excludeChildren(int, boolean)
- * @see #excludeChildren(View, boolean)
- */
- @NonNull
- public Transition excludeChildren(@NonNull Class type, boolean exclude) {
- mTargetTypeChildExcludes = excludeType(mTargetTypeChildExcludes, type, exclude);
- return this;
- }
-
- /**
- * Utility method to manage the boilerplate code that is the same whether we
- * are excluding targets or their children.
- */
- private ArrayList<Class> excludeType(ArrayList<Class> list, Class type, boolean exclude) {
- if (type != null) {
- if (exclude) {
- list = ArrayListManager.add(list, type);
- } else {
- list = ArrayListManager.remove(list, type);
- }
- }
- return list;
- }
-
- /**
- * Returns the array of target IDs that this transition limits itself to
- * tracking and animating. If the array is null for both this method and
- * {@link #getTargets()}, then this transition is
- * not limited to specific views, and will handle changes to any views
- * in the hierarchy of a scene change.
- *
- * @return the list of target IDs
- */
- @NonNull
- public List<Integer> getTargetIds() {
- return mTargetIds;
- }
-
- /**
- * Returns the array of target views that this transition limits itself to
- * tracking and animating. If the array is null for both this method and
- * {@link #getTargetIds()}, then this transition is
- * not limited to specific views, and will handle changes to any views
- * in the hierarchy of a scene change.
- *
- * @return the list of target views
- */
- @NonNull
- public List<View> getTargets() {
- return mTargets;
- }
-
- /**
- * Returns the list of target transitionNames that this transition limits itself to
- * tracking and animating. If the list is null or empty for
- * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
- * {@link #getTargetTypes()} then this transition is
- * not limited to specific views, and will handle changes to any views
- * in the hierarchy of a scene change.
- *
- * @return the list of target transitionNames
- */
- @Nullable
- public List<String> getTargetNames() {
- return mTargetNames;
- }
-
- /**
- * Returns the list of target transitionNames that this transition limits itself to
- * tracking and animating. If the list is null or empty for
- * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
- * {@link #getTargetTypes()} then this transition is
- * not limited to specific views, and will handle changes to any views
- * in the hierarchy of a scene change.
- *
- * @return the list of target Types
- */
- @Nullable
- public List<Class> getTargetTypes() {
- return mTargetTypes;
- }
-
- /**
- * Recursive method that captures values for the given view and the
- * hierarchy underneath it.
- *
- * @param sceneRoot The root of the view hierarchy being captured
- * @param start true if this capture is happening before the scene change,
- * false otherwise
- */
- void captureValues(ViewGroup sceneRoot, boolean start) {
- clearValues(start);
- if ((mTargetIds.size() > 0 || mTargets.size() > 0)
- && (mTargetNames == null || mTargetNames.isEmpty())
- && (mTargetTypes == null || mTargetTypes.isEmpty())) {
- for (int i = 0; i < mTargetIds.size(); ++i) {
- int id = mTargetIds.get(i);
- View view = sceneRoot.findViewById(id);
- if (view != null) {
- TransitionValues values = new TransitionValues();
- values.view = view;
- if (start) {
- captureStartValues(values);
- } else {
- captureEndValues(values);
- }
- values.mTargetedTransitions.add(this);
- capturePropagationValues(values);
- if (start) {
- addViewValues(mStartValues, view, values);
- } else {
- addViewValues(mEndValues, view, values);
- }
- }
- }
- for (int i = 0; i < mTargets.size(); ++i) {
- View view = mTargets.get(i);
- TransitionValues values = new TransitionValues();
- values.view = view;
- if (start) {
- captureStartValues(values);
- } else {
- captureEndValues(values);
- }
- values.mTargetedTransitions.add(this);
- capturePropagationValues(values);
- if (start) {
- addViewValues(mStartValues, view, values);
- } else {
- addViewValues(mEndValues, view, values);
- }
- }
- } else {
- captureHierarchy(sceneRoot, start);
- }
- if (!start && mNameOverrides != null) {
- int numOverrides = mNameOverrides.size();
- ArrayList<View> overriddenViews = new ArrayList<>(numOverrides);
- for (int i = 0; i < numOverrides; i++) {
- String fromName = mNameOverrides.keyAt(i);
- overriddenViews.add(mStartValues.mNameValues.remove(fromName));
- }
- for (int i = 0; i < numOverrides; i++) {
- View view = overriddenViews.get(i);
- if (view != null) {
- String toName = mNameOverrides.valueAt(i);
- mStartValues.mNameValues.put(toName, view);
- }
- }
- }
- }
-
- private static void addViewValues(TransitionValuesMaps transitionValuesMaps,
- View view, TransitionValues transitionValues) {
- transitionValuesMaps.mViewValues.put(view, transitionValues);
- int id = view.getId();
- if (id >= 0) {
- if (transitionValuesMaps.mIdValues.indexOfKey(id) >= 0) {
- // Duplicate IDs cannot match by ID.
- transitionValuesMaps.mIdValues.put(id, null);
- } else {
- transitionValuesMaps.mIdValues.put(id, view);
- }
- }
- String name = ViewCompat.getTransitionName(view);
- if (name != null) {
- if (transitionValuesMaps.mNameValues.containsKey(name)) {
- // Duplicate transitionNames: cannot match by transitionName.
- transitionValuesMaps.mNameValues.put(name, null);
- } else {
- transitionValuesMaps.mNameValues.put(name, view);
- }
- }
- if (view.getParent() instanceof ListView) {
- ListView listview = (ListView) view.getParent();
- if (listview.getAdapter().hasStableIds()) {
- int position = listview.getPositionForView(view);
- long itemId = listview.getItemIdAtPosition(position);
- if (transitionValuesMaps.mItemIdValues.indexOfKey(itemId) >= 0) {
- // Duplicate item IDs: cannot match by item ID.
- View alreadyMatched = transitionValuesMaps.mItemIdValues.get(itemId);
- if (alreadyMatched != null) {
- ViewCompat.setHasTransientState(alreadyMatched, false);
- transitionValuesMaps.mItemIdValues.put(itemId, null);
- }
- } else {
- ViewCompat.setHasTransientState(view, true);
- transitionValuesMaps.mItemIdValues.put(itemId, view);
- }
- }
- }
- }
-
- /**
- * Clear valuesMaps for specified start/end state
- *
- * @param start true if the start values should be cleared, false otherwise
- */
- void clearValues(boolean start) {
- if (start) {
- mStartValues.mViewValues.clear();
- mStartValues.mIdValues.clear();
- mStartValues.mItemIdValues.clear();
- } else {
- mEndValues.mViewValues.clear();
- mEndValues.mIdValues.clear();
- mEndValues.mItemIdValues.clear();
- }
- }
-
- /**
- * Recursive method which captures values for an entire view hierarchy,
- * starting at some root view. Transitions without targetIDs will use this
- * method to capture values for all possible views.
- *
- * @param view The view for which to capture values. Children of this View
- * will also be captured, recursively down to the leaf nodes.
- * @param start true if values are being captured in the start scene, false
- * otherwise.
- */
- private void captureHierarchy(View view, boolean start) {
- if (view == null) {
- return;
- }
- int id = view.getId();
- if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
- return;
- }
- if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
- return;
- }
- if (mTargetTypeExcludes != null) {
- int numTypes = mTargetTypeExcludes.size();
- for (int i = 0; i < numTypes; ++i) {
- if (mTargetTypeExcludes.get(i).isInstance(view)) {
- return;
- }
- }
- }
- if (view.getParent() instanceof ViewGroup) {
- TransitionValues values = new TransitionValues();
- values.view = view;
- if (start) {
- captureStartValues(values);
- } else {
- captureEndValues(values);
- }
- values.mTargetedTransitions.add(this);
- capturePropagationValues(values);
- if (start) {
- addViewValues(mStartValues, view, values);
- } else {
- addViewValues(mEndValues, view, values);
- }
- }
- if (view instanceof ViewGroup) {
- // Don't traverse child hierarchy if there are any child-excludes on this view
- if (mTargetIdChildExcludes != null && mTargetIdChildExcludes.contains(id)) {
- return;
- }
- if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) {
- return;
- }
- if (mTargetTypeChildExcludes != null) {
- int numTypes = mTargetTypeChildExcludes.size();
- for (int i = 0; i < numTypes; ++i) {
- if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
- return;
- }
- }
- }
- ViewGroup parent = (ViewGroup) view;
- for (int i = 0; i < parent.getChildCount(); ++i) {
- captureHierarchy(parent.getChildAt(i), start);
- }
- }
- }
-
- /**
- * This method can be called by transitions to get the TransitionValues for
- * any particular view during the transition-playing process. This might be
- * necessary, for example, to query the before/after state of related views
- * for a given transition.
- */
- @Nullable
- public TransitionValues getTransitionValues(@NonNull View view, boolean start) {
- if (mParent != null) {
- return mParent.getTransitionValues(view, start);
- }
- TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues;
- return valuesMaps.mViewValues.get(view);
- }
-
- /**
- * Find the matched start or end value for a given View. This is only valid
- * after playTransition starts. For example, it will be valid in
- * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)}, but not
- * in {@link #captureStartValues(TransitionValues)}.
- *
- * @param view The view to find the match for.
- * @param viewInStart Is View from the start values or end values.
- * @return The matching TransitionValues for view in either start or end values, depending
- * on viewInStart or null if there is no match for the given view.
- */
- TransitionValues getMatchedTransitionValues(View view, boolean viewInStart) {
- if (mParent != null) {
- return mParent.getMatchedTransitionValues(view, viewInStart);
- }
- ArrayList<TransitionValues> lookIn = viewInStart ? mStartValuesList : mEndValuesList;
- if (lookIn == null) {
- return null;
- }
- int count = lookIn.size();
- int index = -1;
- for (int i = 0; i < count; i++) {
- TransitionValues values = lookIn.get(i);
- if (values == null) {
- return null;
- }
- if (values.view == view) {
- index = i;
- break;
- }
- }
- TransitionValues values = null;
- if (index >= 0) {
- ArrayList<TransitionValues> matchIn = viewInStart ? mEndValuesList : mStartValuesList;
- values = matchIn.get(index);
- }
- return values;
- }
-
- /**
- * Pauses this transition, sending out calls to {@link
- * TransitionListener#onTransitionPause(Transition)} to all listeners
- * and pausing all running animators started by this transition.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void pause(View sceneRoot) {
- if (!mEnded) {
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- int numOldAnims = runningAnimators.size();
- WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
- for (int i = numOldAnims - 1; i >= 0; i--) {
- AnimationInfo info = runningAnimators.valueAt(i);
- if (info.mView != null && windowId.equals(info.mWindowId)) {
- Animator anim = runningAnimators.keyAt(i);
- AnimatorUtils.pause(anim);
- }
- }
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionPause(this);
- }
- }
- mPaused = true;
- }
- }
-
- /**
- * Resumes this transition, sending out calls to {@link
- * TransitionListener#onTransitionPause(Transition)} to all listeners
- * and pausing all running animators started by this transition.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void resume(View sceneRoot) {
- if (mPaused) {
- if (!mEnded) {
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- int numOldAnims = runningAnimators.size();
- WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
- for (int i = numOldAnims - 1; i >= 0; i--) {
- AnimationInfo info = runningAnimators.valueAt(i);
- if (info.mView != null && windowId.equals(info.mWindowId)) {
- Animator anim = runningAnimators.keyAt(i);
- AnimatorUtils.resume(anim);
- }
- }
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionResume(this);
- }
- }
- }
- mPaused = false;
- }
- }
-
- /**
- * Called by TransitionManager to play the transition. This calls
- * createAnimators() to set things up and create all of the animations and then
- * runAnimations() to actually start the animations.
- */
- void playTransition(ViewGroup sceneRoot) {
- mStartValuesList = new ArrayList<>();
- mEndValuesList = new ArrayList<>();
- matchStartAndEnd(mStartValues, mEndValues);
-
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- int numOldAnims = runningAnimators.size();
- WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
- for (int i = numOldAnims - 1; i >= 0; i--) {
- Animator anim = runningAnimators.keyAt(i);
- if (anim != null) {
- AnimationInfo oldInfo = runningAnimators.get(anim);
- if (oldInfo != null && oldInfo.mView != null
- && windowId.equals(oldInfo.mWindowId)) {
- TransitionValues oldValues = oldInfo.mValues;
- View oldView = oldInfo.mView;
- TransitionValues startValues = getTransitionValues(oldView, true);
- TransitionValues endValues = getMatchedTransitionValues(oldView, true);
- boolean cancel = (startValues != null || endValues != null)
- && oldInfo.mTransition.isTransitionRequired(oldValues, endValues);
- if (cancel) {
- if (anim.isRunning() || anim.isStarted()) {
- if (DBG) {
- Log.d(LOG_TAG, "Canceling anim " + anim);
- }
- anim.cancel();
- } else {
- if (DBG) {
- Log.d(LOG_TAG, "removing anim from info list: " + anim);
- }
- runningAnimators.remove(anim);
- }
- }
- }
- }
- }
-
- createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
- runAnimators();
- }
-
- /**
- * Returns whether or not the transition should create an Animator, based on the values
- * captured during {@link #captureStartValues(TransitionValues)} and
- * {@link #captureEndValues(TransitionValues)}. The default implementation compares the
- * property values returned from {@link #getTransitionProperties()}, or all property values if
- * {@code getTransitionProperties()} returns null. Subclasses may override this method to
- * provide logic more specific to the transition implementation.
- *
- * @param startValues the values from captureStartValues, This may be {@code null} if the
- * View did not exist in the start state.
- * @param endValues the values from captureEndValues. This may be {@code null} if the View
- * did not exist in the end state.
- */
- public boolean isTransitionRequired(@Nullable TransitionValues startValues,
- @Nullable TransitionValues endValues) {
- boolean valuesChanged = false;
- // if startValues null, then transition didn't care to stash values,
- // and won't get canceled
- if (startValues != null && endValues != null) {
- String[] properties = getTransitionProperties();
- if (properties != null) {
- for (String property : properties) {
- if (isValueChanged(startValues, endValues, property)) {
- valuesChanged = true;
- break;
- }
- }
- } else {
- for (String key : startValues.values.keySet()) {
- if (isValueChanged(startValues, endValues, key)) {
- valuesChanged = true;
- break;
- }
- }
- }
- }
- return valuesChanged;
- }
-
- private static boolean isValueChanged(TransitionValues oldValues, TransitionValues newValues,
- String key) {
- Object oldValue = oldValues.values.get(key);
- Object newValue = newValues.values.get(key);
- boolean changed;
- if (oldValue == null && newValue == null) {
- // both are null
- changed = false;
- } else if (oldValue == null || newValue == null) {
- // one is null
- changed = true;
- } else {
- // neither is null
- changed = !oldValue.equals(newValue);
- }
- if (DBG && changed) {
- Log.d(LOG_TAG, "Transition.playTransition: "
- + "oldValue != newValue for " + key
- + ": old, new = " + oldValue + ", " + newValue);
- }
- return changed;
- }
-
- /**
- * This is a utility method used by subclasses to handle standard parts of
- * setting up and running an Animator: it sets the {@link #getDuration()
- * duration} and the {@link #getStartDelay() startDelay}, starts the
- * animation, and, when the animator ends, calls {@link #end()}.
- *
- * @param animator The Animator to be run during this transition.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void animate(Animator animator) {
- // TODO: maybe pass auto-end as a boolean parameter?
- if (animator == null) {
- end();
- } else {
- if (getDuration() >= 0) {
- animator.setDuration(getDuration());
- }
- if (getStartDelay() >= 0) {
- animator.setStartDelay(getStartDelay());
- }
- if (getInterpolator() != null) {
- animator.setInterpolator(getInterpolator());
- }
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- end();
- animation.removeListener(this);
- }
- });
- animator.start();
- }
- }
-
- /**
- * This method is called automatically by the transition and
- * TransitionSet classes prior to a Transition subclass starting;
- * subclasses should not need to call it directly.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void start() {
- if (mNumInstances == 0) {
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionStart(this);
- }
- }
- mEnded = false;
- }
- mNumInstances++;
- }
-
- /**
- * This method is called automatically by the Transition and
- * TransitionSet classes when a transition finishes, either because
- * a transition did nothing (returned a null Animator from
- * {@link Transition#createAnimator(ViewGroup, TransitionValues,
- * TransitionValues)}) or because the transition returned a valid
- * Animator and end() was called in the onAnimationEnd()
- * callback of the AnimatorListener.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void end() {
- --mNumInstances;
- if (mNumInstances == 0) {
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionEnd(this);
- }
- }
- for (int i = 0; i < mStartValues.mItemIdValues.size(); ++i) {
- View view = mStartValues.mItemIdValues.valueAt(i);
- if (view != null) {
- ViewCompat.setHasTransientState(view, false);
- }
- }
- for (int i = 0; i < mEndValues.mItemIdValues.size(); ++i) {
- View view = mEndValues.mItemIdValues.valueAt(i);
- if (view != null) {
- ViewCompat.setHasTransientState(view, false);
- }
- }
- mEnded = true;
- }
- }
-
- /**
- * Force the transition to move to its end state, ending all the animators.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- void forceToEnd(ViewGroup sceneRoot) {
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
- int numOldAnims = runningAnimators.size();
- if (sceneRoot != null) {
- WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
- for (int i = numOldAnims - 1; i >= 0; i--) {
- AnimationInfo info = runningAnimators.valueAt(i);
- if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
- Animator anim = runningAnimators.keyAt(i);
- anim.end();
- }
- }
- }
- }
-
- /**
- * This method cancels a transition that is currently running.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected void cancel() {
- int numAnimators = mCurrentAnimators.size();
- for (int i = numAnimators - 1; i >= 0; i--) {
- Animator animator = mCurrentAnimators.get(i);
- animator.cancel();
- }
- if (mListeners != null && mListeners.size() > 0) {
- @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
- (ArrayList<TransitionListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onTransitionCancel(this);
- }
- }
- }
-
- /**
- * Adds a listener to the set of listeners that are sent events through the
- * life of an animation, such as start, repeat, and end.
- *
- * @param listener the listener to be added to the current set of listeners
- * for this animation.
- * @return This transition object.
- */
- @NonNull
- public Transition addListener(@NonNull TransitionListener listener) {
- if (mListeners == null) {
- mListeners = new ArrayList<>();
- }
- mListeners.add(listener);
- return this;
- }
-
- /**
- * Removes a listener from the set listening to this animation.
- *
- * @param listener the listener to be removed from the current set of
- * listeners for this transition.
- * @return This transition object.
- */
- @NonNull
- public Transition removeListener(@NonNull TransitionListener listener) {
- if (mListeners == null) {
- return this;
- }
- mListeners.remove(listener);
- if (mListeners.size() == 0) {
- mListeners = null;
- }
- return this;
- }
-
- /**
- * Sets the algorithm used to calculate two-dimensional interpolation.
- * <p>
- * Transitions such as {@link android.transition.ChangeBounds} move Views, typically
- * in a straight path between the start and end positions. Applications that desire to
- * have these motions move in a curve can change how Views interpolate in two dimensions
- * by extending PathMotion and implementing
- * {@link android.transition.PathMotion#getPath(float, float, float, float)}.
- * </p>
- *
- * @param pathMotion Algorithm object to use for determining how to interpolate in two
- * dimensions. If null, a straight-path algorithm will be used.
- * @see android.transition.ArcMotion
- * @see PatternPathMotion
- * @see android.transition.PathMotion
- */
- public void setPathMotion(@Nullable PathMotion pathMotion) {
- if (pathMotion == null) {
- mPathMotion = STRAIGHT_PATH_MOTION;
- } else {
- mPathMotion = pathMotion;
- }
- }
-
- /**
- * Returns the algorithm object used to interpolate along two dimensions. This is typically
- * used to determine the View motion between two points.
- *
- * @return The algorithm object used to interpolate along two dimensions.
- * @see android.transition.ArcMotion
- * @see PatternPathMotion
- * @see android.transition.PathMotion
- */
- @NonNull
- public PathMotion getPathMotion() {
- return mPathMotion;
- }
-
- /**
- * Sets the callback to use to find the epicenter of a Transition. A null value indicates
- * that there is no epicenter in the Transition and onGetEpicenter() will return null.
- * Transitions like {@link android.transition.Explode} use a point or Rect to orient
- * the direction of travel. This is called the epicenter of the Transition and is
- * typically centered on a touched View. The
- * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
- * dynamically retrieve the epicenter during a Transition.
- *
- * @param epicenterCallback The callback to use to find the epicenter of the Transition.
- */
- public void setEpicenterCallback(@Nullable EpicenterCallback epicenterCallback) {
- mEpicenterCallback = epicenterCallback;
- }
-
- /**
- * Returns the callback used to find the epicenter of the Transition.
- * Transitions like {@link android.transition.Explode} use a point or Rect to orient
- * the direction of travel. This is called the epicenter of the Transition and is
- * typically centered on a touched View. The
- * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
- * dynamically retrieve the epicenter during a Transition.
- *
- * @return the callback used to find the epicenter of the Transition.
- */
- @Nullable
- public EpicenterCallback getEpicenterCallback() {
- return mEpicenterCallback;
- }
-
- /**
- * Returns the epicenter as specified by the
- * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
- *
- * @return the epicenter as specified by the
- * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
- * @see #setEpicenterCallback(EpicenterCallback)
- */
- @Nullable
- public Rect getEpicenter() {
- if (mEpicenterCallback == null) {
- return null;
- }
- return mEpicenterCallback.onGetEpicenter(this);
- }
-
- /**
- * Sets the method for determining Animator start delays.
- * When a Transition affects several Views like {@link android.transition.Explode} or
- * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
- * such that the Animator start delay depends on position of the View. The
- * TransitionPropagation specifies how the start delays are calculated.
- *
- * @param transitionPropagation The class used to determine the start delay of
- * Animators created by this Transition. A null value
- * indicates that no delay should be used.
- */
- public void setPropagation(@Nullable TransitionPropagation transitionPropagation) {
- mPropagation = transitionPropagation;
- }
-
- /**
- * Returns the {@link android.transition.TransitionPropagation} used to calculate Animator
- * start
- * delays.
- * When a Transition affects several Views like {@link android.transition.Explode} or
- * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
- * such that the Animator start delay depends on position of the View. The
- * TransitionPropagation specifies how the start delays are calculated.
- *
- * @return the {@link android.transition.TransitionPropagation} used to calculate Animator start
- * delays. This is null by default.
- */
- @Nullable
- public TransitionPropagation getPropagation() {
- return mPropagation;
- }
-
- /**
- * Captures TransitionPropagation values for the given view and the
- * hierarchy underneath it.
- */
- void capturePropagationValues(TransitionValues transitionValues) {
- if (mPropagation != null && !transitionValues.values.isEmpty()) {
- String[] propertyNames = mPropagation.getPropagationProperties();
- if (propertyNames == null) {
- return;
- }
- boolean containsAll = true;
- for (int i = 0; i < propertyNames.length; i++) {
- if (!transitionValues.values.containsKey(propertyNames[i])) {
- containsAll = false;
- break;
- }
- }
- if (!containsAll) {
- mPropagation.captureValues(transitionValues);
- }
- }
- }
-
- Transition setSceneRoot(ViewGroup sceneRoot) {
- mSceneRoot = sceneRoot;
- return this;
- }
-
- void setCanRemoveViews(boolean canRemoveViews) {
- mCanRemoveViews = canRemoveViews;
- }
-
- @Override
- public String toString() {
- return toString("");
- }
-
- @Override
- public Transition clone() {
- try {
- Transition clone = (Transition) super.clone();
- clone.mAnimators = new ArrayList<>();
- clone.mStartValues = new TransitionValuesMaps();
- clone.mEndValues = new TransitionValuesMaps();
- clone.mStartValuesList = null;
- clone.mEndValuesList = null;
- return clone;
- } catch (CloneNotSupportedException e) {
- return null;
- }
- }
-
- /**
- * Returns the name of this Transition. This name is used internally to distinguish
- * between different transitions to determine when interrupting transitions overlap.
- * For example, a ChangeBounds running on the same target view as another ChangeBounds
- * should determine whether the old transition is animating to different end values
- * and should be canceled in favor of the new transition.
- *
- * <p>By default, a Transition's name is simply the value of {@link Class#getName()},
- * but subclasses are free to override and return something different.</p>
- *
- * @return The name of this transition.
- */
- @NonNull
- public String getName() {
- return mName;
- }
-
- String toString(String indent) {
- String result = indent + getClass().getSimpleName() + "@"
- + Integer.toHexString(hashCode()) + ": ";
- if (mDuration != -1) {
- result += "dur(" + mDuration + ") ";
- }
- if (mStartDelay != -1) {
- result += "dly(" + mStartDelay + ") ";
- }
- if (mInterpolator != null) {
- result += "interp(" + mInterpolator + ") ";
- }
- if (mTargetIds.size() > 0 || mTargets.size() > 0) {
- result += "tgts(";
- if (mTargetIds.size() > 0) {
- for (int i = 0; i < mTargetIds.size(); ++i) {
- if (i > 0) {
- result += ", ";
- }
- result += mTargetIds.get(i);
- }
- }
- if (mTargets.size() > 0) {
- for (int i = 0; i < mTargets.size(); ++i) {
- if (i > 0) {
- result += ", ";
- }
- result += mTargets.get(i);
- }
- }
- result += ")";
- }
- return result;
- }
-
- /**
- * A transition listener receives notifications from a transition.
- * Notifications indicate transition lifecycle events.
- */
- public interface TransitionListener {
-
- /**
- * Notification about the start of the transition.
- *
- * @param transition The started transition.
- */
- void onTransitionStart(@NonNull Transition transition);
-
- /**
- * Notification about the end of the transition. Canceled transitions
- * will always notify listeners of both the cancellation and end
- * events. That is, {@link #onTransitionEnd(Transition)} is always called,
- * regardless of whether the transition was canceled or played
- * through to completion.
- *
- * @param transition The transition which reached its end.
- */
- void onTransitionEnd(@NonNull Transition transition);
-
- /**
- * Notification about the cancellation of the transition.
- * Note that cancel may be called by a parent {@link TransitionSet} on
- * a child transition which has not yet started. This allows the child
- * transition to restore state on target objects which was set at
- * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
- * createAnimator()} time.
- *
- * @param transition The transition which was canceled.
- */
- void onTransitionCancel(@NonNull Transition transition);
-
- /**
- * Notification when a transition is paused.
- * Note that createAnimator() may be called by a parent {@link TransitionSet} on
- * a child transition which has not yet started. This allows the child
- * transition to restore state on target objects which was set at
- * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
- * createAnimator()} time.
- *
- * @param transition The transition which was paused.
- */
- void onTransitionPause(@NonNull Transition transition);
-
- /**
- * Notification when a transition is resumed.
- * Note that resume() may be called by a parent {@link TransitionSet} on
- * a child transition which has not yet started. This allows the child
- * transition to restore state which may have changed in an earlier call
- * to {@link #onTransitionPause(Transition)}.
- *
- * @param transition The transition which was resumed.
- */
- void onTransitionResume(@NonNull Transition transition);
- }
-
- /**
- * Holds information about each animator used when a new transition starts
- * while other transitions are still running to determine whether a running
- * animation should be canceled or a new animation noop'd. The structure holds
- * information about the state that an animation is going to, to be compared to
- * end state of a new animation.
- */
- private static class AnimationInfo {
-
- View mView;
-
- String mName;
-
- TransitionValues mValues;
-
- WindowIdImpl mWindowId;
-
- Transition mTransition;
-
- AnimationInfo(View view, String name, Transition transition, WindowIdImpl windowId,
- TransitionValues values) {
- mView = view;
- mName = name;
- mValues = values;
- mWindowId = windowId;
- mTransition = transition;
- }
- }
-
- /**
- * Utility class for managing typed ArrayLists efficiently. In particular, this
- * can be useful for lists that we don't expect to be used often (eg, the exclude
- * lists), so we'd like to keep them nulled out by default. This causes the code to
- * become tedious, with constant null checks, code to allocate when necessary,
- * and code to null out the reference when the list is empty. This class encapsulates
- * all of that functionality into simple add()/remove() methods which perform the
- * necessary checks, allocation/null-out as appropriate, and return the
- * resulting list.
- */
- private static class ArrayListManager {
-
- /**
- * Add the specified item to the list, returning the resulting list.
- * The returned list can either the be same list passed in or, if that
- * list was null, the new list that was created.
- *
- * Note that the list holds unique items; if the item already exists in the
- * list, the list is not modified.
- */
- static <T> ArrayList<T> add(ArrayList<T> list, T item) {
- if (list == null) {
- list = new ArrayList<>();
- }
- if (!list.contains(item)) {
- list.add(item);
- }
- return list;
- }
-
- /**
- * Remove the specified item from the list, returning the resulting list.
- * The returned list can either the be same list passed in or, if that
- * list becomes empty as a result of the remove(), the new list was created.
- */
- static <T> ArrayList<T> remove(ArrayList<T> list, T item) {
- if (list != null) {
- list.remove(item);
- if (list.isEmpty()) {
- list = null;
- }
- }
- return list;
- }
- }
-
- /**
- * Class to get the epicenter of Transition. Use
- * {@link #setEpicenterCallback(EpicenterCallback)} to set the callback used to calculate the
- * epicenter of the Transition. Override {@link #getEpicenter()} to return the rectangular
- * region in screen coordinates of the epicenter of the transition.
- *
- * @see #setEpicenterCallback(EpicenterCallback)
- */
- public abstract static class EpicenterCallback {
-
- /**
- * Implementers must override to return the epicenter of the Transition in screen
- * coordinates. Transitions like {@link android.transition.Explode} depend upon
- * an epicenter for the Transition. In Explode, Views move toward or away from the
- * center of the epicenter Rect along the vector between the epicenter and the center
- * of the View appearing and disappearing. Some Transitions, such as
- * {@link android.transition.Fade} pay no attention to the epicenter.
- *
- * @param transition The transition for which the epicenter applies.
- * @return The Rect region of the epicenter of <code>transition</code> or null if
- * there is no epicenter.
- */
- public abstract Rect onGetEpicenter(@NonNull Transition transition);
- }
-
-}
diff --git a/transition/src/android/support/transition/package.html b/transition/src/android/support/transition/package.html
deleted file mode 100644
index b09005f..0000000
--- a/transition/src/android/support/transition/package.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<body>
-
-Support android.transition classes to provide transition API back to android API level 14.
-This library contains {@link android.support.transition.Transition},
-{@link android.support.transition.TransitionManager}, and other related classes
-back-ported from their platform versions introduced Android API level 19.
-
-</body>
diff --git a/transition/src/android/support/transition/AnimatorUtils.java b/transition/src/main/java/android/support/transition/AnimatorUtils.java
similarity index 100%
rename from transition/src/android/support/transition/AnimatorUtils.java
rename to transition/src/main/java/android/support/transition/AnimatorUtils.java
diff --git a/transition/api14/android/support/transition/AnimatorUtilsApi14.java b/transition/src/main/java/android/support/transition/AnimatorUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/AnimatorUtilsApi14.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsApi14.java
diff --git a/transition/api19/android/support/transition/AnimatorUtilsApi19.java b/transition/src/main/java/android/support/transition/AnimatorUtilsApi19.java
similarity index 100%
rename from transition/api19/android/support/transition/AnimatorUtilsApi19.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsApi19.java
diff --git a/transition/base/android/support/transition/AnimatorUtilsImpl.java b/transition/src/main/java/android/support/transition/AnimatorUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/AnimatorUtilsImpl.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsImpl.java
diff --git a/transition/src/android/support/transition/ArcMotion.java b/transition/src/main/java/android/support/transition/ArcMotion.java
similarity index 100%
rename from transition/src/android/support/transition/ArcMotion.java
rename to transition/src/main/java/android/support/transition/ArcMotion.java
diff --git a/transition/src/main/java/android/support/transition/AutoTransition.java b/transition/src/main/java/android/support/transition/AutoTransition.java
new file mode 100644
index 0000000..bf39c3c
--- /dev/null
+++ b/transition/src/main/java/android/support/transition/AutoTransition.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * Utility class for creating a default transition that automatically fades,
+ * moves, and resizes views during a scene change.
+ *
+ * <p>An AutoTransition can be described in a resource file by using the
+ * tag <code>autoTransition</code>, along with the other standard
+ * attributes of {@link Transition}.</p>
+ */
+public class AutoTransition extends TransitionSet {
+
+ /**
+ * Constructs an AutoTransition object, which is a TransitionSet which
+ * first fades out disappearing targets, then moves and resizes existing
+ * targets, and finally fades in appearing targets.
+ */
+ public AutoTransition() {
+ init();
+ }
+
+ public AutoTransition(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ setOrdering(ORDERING_SEQUENTIAL);
+ addTransition(new Fade(Fade.OUT))
+ .addTransition(new ChangeBounds())
+ .addTransition(new Fade(Fade.IN));
+ }
+
+}
diff --git a/transition/src/android/support/transition/ChangeBounds.java b/transition/src/main/java/android/support/transition/ChangeBounds.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeBounds.java
rename to transition/src/main/java/android/support/transition/ChangeBounds.java
diff --git a/transition/src/android/support/transition/ChangeClipBounds.java b/transition/src/main/java/android/support/transition/ChangeClipBounds.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeClipBounds.java
rename to transition/src/main/java/android/support/transition/ChangeClipBounds.java
diff --git a/transition/src/android/support/transition/ChangeImageTransform.java b/transition/src/main/java/android/support/transition/ChangeImageTransform.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeImageTransform.java
rename to transition/src/main/java/android/support/transition/ChangeImageTransform.java
diff --git a/transition/src/android/support/transition/ChangeScroll.java b/transition/src/main/java/android/support/transition/ChangeScroll.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeScroll.java
rename to transition/src/main/java/android/support/transition/ChangeScroll.java
diff --git a/transition/src/android/support/transition/ChangeTransform.java b/transition/src/main/java/android/support/transition/ChangeTransform.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeTransform.java
rename to transition/src/main/java/android/support/transition/ChangeTransform.java
diff --git a/transition/src/android/support/transition/CircularPropagation.java b/transition/src/main/java/android/support/transition/CircularPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/CircularPropagation.java
rename to transition/src/main/java/android/support/transition/CircularPropagation.java
diff --git a/transition/src/android/support/transition/Explode.java b/transition/src/main/java/android/support/transition/Explode.java
similarity index 100%
rename from transition/src/android/support/transition/Explode.java
rename to transition/src/main/java/android/support/transition/Explode.java
diff --git a/transition/src/android/support/transition/Fade.java b/transition/src/main/java/android/support/transition/Fade.java
similarity index 100%
rename from transition/src/android/support/transition/Fade.java
rename to transition/src/main/java/android/support/transition/Fade.java
diff --git a/transition/src/android/support/transition/FloatArrayEvaluator.java b/transition/src/main/java/android/support/transition/FloatArrayEvaluator.java
similarity index 100%
rename from transition/src/android/support/transition/FloatArrayEvaluator.java
rename to transition/src/main/java/android/support/transition/FloatArrayEvaluator.java
diff --git a/transition/src/android/support/transition/FragmentTransitionSupport.java b/transition/src/main/java/android/support/transition/FragmentTransitionSupport.java
similarity index 100%
rename from transition/src/android/support/transition/FragmentTransitionSupport.java
rename to transition/src/main/java/android/support/transition/FragmentTransitionSupport.java
diff --git a/transition/api14/android/support/transition/GhostViewApi14.java b/transition/src/main/java/android/support/transition/GhostViewApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/GhostViewApi14.java
rename to transition/src/main/java/android/support/transition/GhostViewApi14.java
diff --git a/transition/api21/android/support/transition/GhostViewApi21.java b/transition/src/main/java/android/support/transition/GhostViewApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/GhostViewApi21.java
rename to transition/src/main/java/android/support/transition/GhostViewApi21.java
diff --git a/transition/base/android/support/transition/GhostViewImpl.java b/transition/src/main/java/android/support/transition/GhostViewImpl.java
similarity index 100%
rename from transition/base/android/support/transition/GhostViewImpl.java
rename to transition/src/main/java/android/support/transition/GhostViewImpl.java
diff --git a/transition/src/android/support/transition/GhostViewUtils.java b/transition/src/main/java/android/support/transition/GhostViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/GhostViewUtils.java
rename to transition/src/main/java/android/support/transition/GhostViewUtils.java
diff --git a/transition/src/android/support/transition/ImageViewUtils.java b/transition/src/main/java/android/support/transition/ImageViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ImageViewUtils.java
rename to transition/src/main/java/android/support/transition/ImageViewUtils.java
diff --git a/transition/api14/android/support/transition/ImageViewUtilsApi14.java b/transition/src/main/java/android/support/transition/ImageViewUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ImageViewUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsApi14.java
diff --git a/transition/api21/android/support/transition/ImageViewUtilsApi21.java b/transition/src/main/java/android/support/transition/ImageViewUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ImageViewUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsApi21.java
diff --git a/transition/base/android/support/transition/ImageViewUtilsImpl.java b/transition/src/main/java/android/support/transition/ImageViewUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ImageViewUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsImpl.java
diff --git a/transition/src/android/support/transition/MatrixUtils.java b/transition/src/main/java/android/support/transition/MatrixUtils.java
similarity index 100%
rename from transition/src/android/support/transition/MatrixUtils.java
rename to transition/src/main/java/android/support/transition/MatrixUtils.java
diff --git a/transition/src/android/support/transition/ObjectAnimatorUtils.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ObjectAnimatorUtils.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtils.java
diff --git a/transition/api14/android/support/transition/ObjectAnimatorUtilsApi14.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ObjectAnimatorUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi14.java
diff --git a/transition/api21/android/support/transition/ObjectAnimatorUtilsApi21.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ObjectAnimatorUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi21.java
diff --git a/transition/base/android/support/transition/ObjectAnimatorUtilsImpl.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ObjectAnimatorUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsImpl.java
diff --git a/transition/src/android/support/transition/PathMotion.java b/transition/src/main/java/android/support/transition/PathMotion.java
similarity index 100%
rename from transition/src/android/support/transition/PathMotion.java
rename to transition/src/main/java/android/support/transition/PathMotion.java
diff --git a/transition/api14/android/support/transition/PathProperty.java b/transition/src/main/java/android/support/transition/PathProperty.java
similarity index 100%
rename from transition/api14/android/support/transition/PathProperty.java
rename to transition/src/main/java/android/support/transition/PathProperty.java
diff --git a/transition/src/android/support/transition/PatternPathMotion.java b/transition/src/main/java/android/support/transition/PatternPathMotion.java
similarity index 100%
rename from transition/src/android/support/transition/PatternPathMotion.java
rename to transition/src/main/java/android/support/transition/PatternPathMotion.java
diff --git a/transition/src/android/support/transition/PropertyValuesHolderUtils.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtils.java
similarity index 100%
rename from transition/src/android/support/transition/PropertyValuesHolderUtils.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtils.java
diff --git a/transition/api14/android/support/transition/PropertyValuesHolderUtilsApi14.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/PropertyValuesHolderUtilsApi14.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi14.java
diff --git a/transition/api21/android/support/transition/PropertyValuesHolderUtilsApi21.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/PropertyValuesHolderUtilsApi21.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi21.java
diff --git a/transition/base/android/support/transition/PropertyValuesHolderUtilsImpl.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/PropertyValuesHolderUtilsImpl.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsImpl.java
diff --git a/transition/src/android/support/transition/RectEvaluator.java b/transition/src/main/java/android/support/transition/RectEvaluator.java
similarity index 100%
rename from transition/src/android/support/transition/RectEvaluator.java
rename to transition/src/main/java/android/support/transition/RectEvaluator.java
diff --git a/transition/src/android/support/transition/Scene.java b/transition/src/main/java/android/support/transition/Scene.java
similarity index 100%
rename from transition/src/android/support/transition/Scene.java
rename to transition/src/main/java/android/support/transition/Scene.java
diff --git a/transition/src/android/support/transition/SidePropagation.java b/transition/src/main/java/android/support/transition/SidePropagation.java
similarity index 100%
rename from transition/src/android/support/transition/SidePropagation.java
rename to transition/src/main/java/android/support/transition/SidePropagation.java
diff --git a/transition/src/android/support/transition/Slide.java b/transition/src/main/java/android/support/transition/Slide.java
similarity index 100%
rename from transition/src/android/support/transition/Slide.java
rename to transition/src/main/java/android/support/transition/Slide.java
diff --git a/transition/src/android/support/transition/Styleable.java b/transition/src/main/java/android/support/transition/Styleable.java
similarity index 100%
rename from transition/src/android/support/transition/Styleable.java
rename to transition/src/main/java/android/support/transition/Styleable.java
diff --git a/transition/src/main/java/android/support/transition/Transition.java b/transition/src/main/java/android/support/transition/Transition.java
new file mode 100644
index 0000000..9c198a9
--- /dev/null
+++ b/transition/src/main/java/android/support/transition/Transition.java
@@ -0,0 +1,2437 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.content.res.TypedArrayUtils;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.LongSparseArray;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.InflateException;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.ListView;
+import android.widget.Spinner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * A Transition holds information about animations that will be run on its
+ * targets during a scene change. Subclasses of this abstract class may
+ * choreograph several child transitions ({@link TransitionSet} or they may
+ * perform custom animations themselves. Any Transition has two main jobs:
+ * (1) capture property values, and (2) play animations based on changes to
+ * captured property values. A custom transition knows what property values
+ * on View objects are of interest to it, and also knows how to animate
+ * changes to those values. For example, the {@link Fade} transition tracks
+ * changes to visibility-related properties and is able to construct and run
+ * animations that fade items in or out based on changes to those properties.
+ *
+ * <p>Note: Transitions may not work correctly with either {@link SurfaceView}
+ * or {@link TextureView}, due to the way that these views are displayed
+ * on the screen. For SurfaceView, the problem is that the view is updated from
+ * a non-UI thread, so changes to the view due to transitions (such as moving
+ * and resizing the view) may be out of sync with the display inside those bounds.
+ * TextureView is more compatible with transitions in general, but some
+ * specific transitions (such as {@link Fade}) may not be compatible
+ * with TextureView because they rely on {@link android.view.ViewOverlay}
+ * functionality, which does not currently work with TextureView.</p>
+ *
+ * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code>
+ * directory. Transition resources consist of a tag name for one of the Transition
+ * subclasses along with attributes to define some of the attributes of that transition.
+ * For example, here is a minimal resource file that declares a {@link ChangeBounds}
+ * transition:</p>
+ *
+ * <pre>
+ * <changeBounds/>
+ * </pre>
+ *
+ * <p>Note that attributes for the transition are not required, just as they are
+ * optional when declared in code; Transitions created from XML resources will use
+ * the same defaults as their code-created equivalents. Here is a slightly more
+ * elaborate example which declares a {@link TransitionSet} transition with
+ * {@link ChangeBounds} and {@link Fade} child transitions:</p>
+ *
+ * <pre>
+ * <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:transitionOrdering="sequential">
+ * <changeBounds/>
+ * <fade android:fadingMode="fade_out">
+ * <targets>
+ * <target android:targetId="@id/grayscaleContainer"/>
+ * </targets>
+ * </fade>
+ * </transitionSet>
+ * </pre>
+ *
+ * <p>In this example, the transitionOrdering attribute is used on the TransitionSet
+ * object to change from the default {@link TransitionSet#ORDERING_TOGETHER} behavior
+ * to be {@link TransitionSet#ORDERING_SEQUENTIAL} instead. Also, the {@link Fade}
+ * transition uses a fadingMode of {@link Fade#OUT} instead of the default
+ * out-in behavior. Finally, note the use of the <code>targets</code> sub-tag, which
+ * takes a set of {code target} tags, each of which lists a specific <code>targetId</code> which
+ * this transition acts upon. Use of targets is optional, but can be used to either limit the time
+ * spent checking attributes on unchanging views, or limiting the types of animations run on
+ * specific views. In this case, we know that only the <code>grayscaleContainer</code> will be
+ * disappearing, so we choose to limit the {@link Fade} transition to only that view.</p>
+ */
+public abstract class Transition implements Cloneable {
+
+ private static final String LOG_TAG = "Transition";
+ static final boolean DBG = false;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by View instance.
+ */
+ public static final int MATCH_INSTANCE = 0x1;
+ private static final int MATCH_FIRST = MATCH_INSTANCE;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by
+ * {@link android.view.View#getTransitionName()}. Null names will not be matched.
+ */
+ public static final int MATCH_NAME = 0x2;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by
+ * {@link android.view.View#getId()}. Negative IDs will not be matched.
+ */
+ public static final int MATCH_ID = 0x3;
+
+ /**
+ * With {@link #setMatchOrder(int...)}, chooses to match by the {@link android.widget.Adapter}
+ * item id. When {@link android.widget.Adapter#hasStableIds()} returns false, no match
+ * will be made for items.
+ */
+ public static final int MATCH_ITEM_ID = 0x4;
+
+ private static final int MATCH_LAST = MATCH_ITEM_ID;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({MATCH_INSTANCE, MATCH_NAME, MATCH_ID, MATCH_ITEM_ID})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MatchOrder {
+ }
+
+ private static final String MATCH_INSTANCE_STR = "instance";
+ private static final String MATCH_NAME_STR = "name";
+ private static final String MATCH_ID_STR = "id";
+ private static final String MATCH_ITEM_ID_STR = "itemId";
+
+ private static final int[] DEFAULT_MATCH_ORDER = {
+ MATCH_NAME,
+ MATCH_INSTANCE,
+ MATCH_ID,
+ MATCH_ITEM_ID,
+ };
+
+ private static final PathMotion STRAIGHT_PATH_MOTION = new PathMotion() {
+ @Override
+ public Path getPath(float startX, float startY, float endX, float endY) {
+ Path path = new Path();
+ path.moveTo(startX, startY);
+ path.lineTo(endX, endY);
+ return path;
+ }
+ };
+
+ private String mName = getClass().getName();
+
+ private long mStartDelay = -1;
+ long mDuration = -1;
+ private TimeInterpolator mInterpolator = null;
+ ArrayList<Integer> mTargetIds = new ArrayList<>();
+ ArrayList<View> mTargets = new ArrayList<>();
+ private ArrayList<String> mTargetNames = null;
+ private ArrayList<Class> mTargetTypes = null;
+ private ArrayList<Integer> mTargetIdExcludes = null;
+ private ArrayList<View> mTargetExcludes = null;
+ private ArrayList<Class> mTargetTypeExcludes = null;
+ private ArrayList<String> mTargetNameExcludes = null;
+ private ArrayList<Integer> mTargetIdChildExcludes = null;
+ private ArrayList<View> mTargetChildExcludes = null;
+ private ArrayList<Class> mTargetTypeChildExcludes = null;
+ private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
+ private TransitionValuesMaps mEndValues = new TransitionValuesMaps();
+ TransitionSet mParent = null;
+ private int[] mMatchOrder = DEFAULT_MATCH_ORDER;
+ private ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
+ private ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts
+
+ // Per-animator information used for later canceling when future transitions overlap
+ private static ThreadLocal<ArrayMap<Animator, Transition.AnimationInfo>> sRunningAnimators =
+ new ThreadLocal<>();
+
+ // Scene Root is set at createAnimator() time in the cloned Transition
+ private ViewGroup mSceneRoot = null;
+
+ // Whether removing views from their parent is possible. This is only for views
+ // in the start scene, which are no longer in the view hierarchy. This property
+ // is determined by whether the previous Scene was created from a layout
+ // resource, and thus the views from the exited scene are going away anyway
+ // and can be removed as necessary to achieve a particular effect, such as
+ // removing them from parents to add them to overlays.
+ boolean mCanRemoveViews = false;
+
+ // Track all animators in use in case the transition gets canceled and needs to
+ // cancel running animators
+ private ArrayList<Animator> mCurrentAnimators = new ArrayList<>();
+
+ // Number of per-target instances of this Transition currently running. This count is
+ // determined by calls to start() and end()
+ private int mNumInstances = 0;
+
+ // Whether this transition is currently paused, due to a call to pause()
+ private boolean mPaused = false;
+
+ // Whether this transition has ended. Used to avoid pause/resume on transitions
+ // that have completed
+ private boolean mEnded = false;
+
+ // The set of listeners to be sent transition lifecycle events.
+ private ArrayList<Transition.TransitionListener> mListeners = null;
+
+ // The set of animators collected from calls to createAnimator(),
+ // to be run in runAnimators()
+ private ArrayList<Animator> mAnimators = new ArrayList<>();
+
+ // The function for calculating the Animation start delay.
+ TransitionPropagation mPropagation;
+
+ // The rectangular region for Transitions like Explode and TransitionPropagations
+ // like CircularPropagation
+ private EpicenterCallback mEpicenterCallback;
+
+ // For Fragment shared element transitions, linking views explicitly by mismatching
+ // transitionNames.
+ private ArrayMap<String, String> mNameOverrides;
+
+ // The function used to interpolate along two-dimensional points. Typically used
+ // for adding curves to x/y View motion.
+ private PathMotion mPathMotion = STRAIGHT_PATH_MOTION;
+
+ /**
+ * Constructs a Transition object with no target objects. A transition with
+ * no targets defaults to running on all target objects in the scene hierarchy
+ * (if the transition is not contained in a TransitionSet), or all target
+ * objects passed down from its parent (if it is in a TransitionSet).
+ */
+ public Transition() {
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style from a
+ * theme attribute or style resource. This constructor of Transition allows
+ * subclasses to use their own base style when they are inflating.
+ *
+ * @param context The Context the transition is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the transition.
+ */
+ public Transition(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION);
+ XmlResourceParser parser = (XmlResourceParser) attrs;
+ long duration = TypedArrayUtils.getNamedInt(a, parser, "duration",
+ Styleable.Transition.DURATION, -1);
+ if (duration >= 0) {
+ setDuration(duration);
+ }
+ long startDelay = TypedArrayUtils.getNamedInt(a, parser, "startDelay",
+ Styleable.Transition.START_DELAY, -1);
+ if (startDelay > 0) {
+ setStartDelay(startDelay);
+ }
+ final int resId = TypedArrayUtils.getNamedResourceId(a, parser, "interpolator",
+ Styleable.Transition.INTERPOLATOR, 0);
+ if (resId > 0) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resId));
+ }
+ String matchOrder = TypedArrayUtils.getNamedString(a, parser, "matchOrder",
+ Styleable.Transition.MATCH_ORDER);
+ if (matchOrder != null) {
+ setMatchOrder(parseMatchOrder(matchOrder));
+ }
+ a.recycle();
+ }
+
+ @MatchOrder
+ private static int[] parseMatchOrder(String matchOrderString) {
+ StringTokenizer st = new StringTokenizer(matchOrderString, ",");
+ @MatchOrder
+ int[] matches = new int[st.countTokens()];
+ int index = 0;
+ while (st.hasMoreTokens()) {
+ String token = st.nextToken().trim();
+ if (MATCH_ID_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_ID;
+ } else if (MATCH_INSTANCE_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_INSTANCE;
+ } else if (MATCH_NAME_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_NAME;
+ } else if (MATCH_ITEM_ID_STR.equalsIgnoreCase(token)) {
+ matches[index] = Transition.MATCH_ITEM_ID;
+ } else if (token.isEmpty()) {
+ @MatchOrder
+ int[] smallerMatches = new int[matches.length - 1];
+ System.arraycopy(matches, 0, smallerMatches, 0, index);
+ matches = smallerMatches;
+ index--;
+ } else {
+ throw new InflateException("Unknown match type in matchOrder: '" + token + "'");
+ }
+ index++;
+ }
+ return matches;
+ }
+
+ /**
+ * Sets the duration of this transition. By default, there is no duration
+ * (indicated by a negative number), which means that the Animator created by
+ * the transition will have its own specified duration. If the duration of a
+ * Transition is set, that duration will override the Animator duration.
+ *
+ * @param duration The length of the animation, in milliseconds.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setDuration(long duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Returns the duration set on this transition. If no duration has been set,
+ * the returned value will be negative, indicating that resulting animators will
+ * retain their own durations.
+ *
+ * @return The duration set on this transition, in milliseconds, if one has been
+ * set, otherwise returns a negative number.
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Sets the startDelay of this transition. By default, there is no delay
+ * (indicated by a negative number), which means that the Animator created by
+ * the transition will have its own specified startDelay. If the delay of a
+ * Transition is set, that delay will override the Animator delay.
+ *
+ * @param startDelay The length of the delay, in milliseconds.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setStartDelay(long startDelay) {
+ mStartDelay = startDelay;
+ return this;
+ }
+
+ /**
+ * Returns the startDelay set on this transition. If no startDelay has been set,
+ * the returned value will be negative, indicating that resulting animators will
+ * retain their own startDelays.
+ *
+ * @return The startDelay set on this transition, in milliseconds, if one has
+ * been set, otherwise returns a negative number.
+ */
+ public long getStartDelay() {
+ return mStartDelay;
+ }
+
+ /**
+ * Sets the interpolator of this transition. By default, the interpolator
+ * is null, which means that the Animator created by the transition
+ * will have its own specified interpolator. If the interpolator of a
+ * Transition is set, that interpolator will override the Animator interpolator.
+ *
+ * @param interpolator The time interpolator used by the transition
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition setInterpolator(@Nullable TimeInterpolator interpolator) {
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Returns the interpolator set on this transition. If no interpolator has been set,
+ * the returned value will be null, indicating that resulting animators will
+ * retain their own interpolators.
+ *
+ * @return The interpolator set on this transition, if one has been set, otherwise
+ * returns null.
+ */
+ @Nullable
+ public TimeInterpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Returns the set of property names used stored in the {@link TransitionValues}
+ * object passed into {@link #captureStartValues(TransitionValues)} that
+ * this transition cares about for the purposes of canceling overlapping animations.
+ * When any transition is started on a given scene root, all transitions
+ * currently running on that same scene root are checked to see whether the
+ * properties on which they based their animations agree with the end values of
+ * the same properties in the new transition. If the end values are not equal,
+ * then the old animation is canceled since the new transition will start a new
+ * animation to these new values. If the values are equal, the old animation is
+ * allowed to continue and no new animation is started for that transition.
+ *
+ * <p>A transition does not need to override this method. However, not doing so
+ * will mean that the cancellation logic outlined in the previous paragraph
+ * will be skipped for that transition, possibly leading to artifacts as
+ * old transitions and new transitions on the same targets run in parallel,
+ * animating views toward potentially different end values.</p>
+ *
+ * @return An array of property names as described in the class documentation for
+ * {@link TransitionValues}. The default implementation returns <code>null</code>.
+ */
+ @Nullable
+ public String[] getTransitionProperties() {
+ return null;
+ }
+
+ /**
+ * This method creates an animation that will be run for this transition
+ * given the information in the startValues and endValues structures captured
+ * earlier for the start and end scenes. Subclasses of Transition should override
+ * this method. The method should only be called by the transition system; it is
+ * not intended to be called from external classes.
+ *
+ * <p>This method is called by the transition's parent (all the way up to the
+ * topmost Transition in the hierarchy) with the sceneRoot and start/end
+ * values that the transition may need to set up initial target values
+ * and construct an appropriate animation. For example, if an overall
+ * Transition is a {@link TransitionSet} consisting of several
+ * child transitions in sequence, then some of the child transitions may
+ * want to set initial values on target views prior to the overall
+ * Transition commencing, to put them in an appropriate state for the
+ * delay between that start and the child Transition start time. For
+ * example, a transition that fades an item in may wish to set the starting
+ * alpha value to 0, to avoid it blinking in prior to the transition
+ * actually starting the animation. This is necessary because the scene
+ * change that triggers the Transition will automatically set the end-scene
+ * on all target views, so a Transition that wants to animate from a
+ * different value should set that value prior to returning from this method.</p>
+ *
+ * <p>Additionally, a Transition can perform logic to determine whether
+ * the transition needs to run on the given target and start/end values.
+ * For example, a transition that resizes objects on the screen may wish
+ * to avoid running for views which are not present in either the start
+ * or end scenes.</p>
+ *
+ * <p>If there is an animator created and returned from this method, the
+ * transition mechanism will apply any applicable duration, startDelay,
+ * and interpolator to that animation and start it. A return value of
+ * <code>null</code> indicates that no animation should run. The default
+ * implementation returns null.</p>
+ *
+ * <p>The method is called for every applicable target object, which is
+ * stored in the {@link TransitionValues#view} field.</p>
+ *
+ * @param sceneRoot The root of the transition hierarchy.
+ * @param startValues The values for a specific target in the start scene.
+ * @param endValues The values for the target in the end scene.
+ * @return A Animator to be started at the appropriate time in the
+ * overall transition for this scene change. A null value means no animation
+ * should be run.
+ */
+ @Nullable
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ return null;
+ }
+
+ /**
+ * Sets the order in which Transition matches View start and end values.
+ * <p>
+ * The default behavior is to match first by {@link android.view.View#getTransitionName()},
+ * then by View instance, then by {@link android.view.View#getId()} and finally
+ * by its item ID if it is in a direct child of ListView. The caller can
+ * choose to have only some or all of the values of {@link #MATCH_INSTANCE},
+ * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. Only
+ * the match algorithms supplied will be used to determine whether Views are the
+ * the same in both the start and end Scene. Views that do not match will be considered
+ * as entering or leaving the Scene.
+ * </p>
+ *
+ * @param matches A list of zero or more of {@link #MATCH_INSTANCE},
+ * {@link #MATCH_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}.
+ * If none are provided, then the default match order will be set.
+ */
+ public void setMatchOrder(@MatchOrder int... matches) {
+ if (matches == null || matches.length == 0) {
+ mMatchOrder = DEFAULT_MATCH_ORDER;
+ } else {
+ for (int i = 0; i < matches.length; i++) {
+ int match = matches[i];
+ if (!isValidMatch(match)) {
+ throw new IllegalArgumentException("matches contains invalid value");
+ }
+ if (alreadyContains(matches, i)) {
+ throw new IllegalArgumentException("matches contains a duplicate value");
+ }
+ }
+ mMatchOrder = matches.clone();
+ }
+ }
+
+ private static boolean isValidMatch(int match) {
+ return (match >= MATCH_FIRST && match <= MATCH_LAST);
+ }
+
+ private static boolean alreadyContains(int[] array, int searchIndex) {
+ int value = array[searchIndex];
+ for (int i = 0; i < searchIndex; i++) {
+ if (array[i] == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Match start/end values by View instance. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd.
+ */
+ private void matchInstances(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd) {
+ for (int i = unmatchedStart.size() - 1; i >= 0; i--) {
+ View view = unmatchedStart.keyAt(i);
+ if (view != null && isValidTarget(view)) {
+ TransitionValues end = unmatchedEnd.remove(view);
+ if (end != null && end.view != null && isValidTarget(end.view)) {
+ TransitionValues start = unmatchedStart.removeAt(i);
+ mStartValuesList.add(start);
+ mEndValuesList.add(end);
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter item ID. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startItemIds and endItemIds as a guide for which Views have unique item IDs.
+ */
+ private void matchItemIds(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ LongSparseArray<View> startItemIds, LongSparseArray<View> endItemIds) {
+ int numStartIds = startItemIds.size();
+ for (int i = 0; i < numStartIds; i++) {
+ View startView = startItemIds.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endItemIds.get(startItemIds.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter view ID. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startIds and endIds as a guide for which Views have unique IDs.
+ */
+ private void matchIds(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ SparseArray<View> startIds, SparseArray<View> endIds) {
+ int numStartIds = startIds.size();
+ for (int i = 0; i < numStartIds; i++) {
+ View startView = startIds.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endIds.get(startIds.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Match start/end values by Adapter transitionName. Adds matched values to mStartValuesList
+ * and mEndValuesList and removes them from unmatchedStart and unmatchedEnd, using
+ * startNames and endNames as a guide for which Views have unique transitionNames.
+ */
+ private void matchNames(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd,
+ ArrayMap<String, View> startNames, ArrayMap<String, View> endNames) {
+ int numStartNames = startNames.size();
+ for (int i = 0; i < numStartNames; i++) {
+ View startView = startNames.valueAt(i);
+ if (startView != null && isValidTarget(startView)) {
+ View endView = endNames.get(startNames.keyAt(i));
+ if (endView != null && isValidTarget(endView)) {
+ TransitionValues startValues = unmatchedStart.get(startView);
+ TransitionValues endValues = unmatchedEnd.get(endView);
+ if (startValues != null && endValues != null) {
+ mStartValuesList.add(startValues);
+ mEndValuesList.add(endValues);
+ unmatchedStart.remove(startView);
+ unmatchedEnd.remove(endView);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds all values from unmatchedStart and unmatchedEnd to mStartValuesList and mEndValuesList,
+ * assuming that there is no match between values in the list.
+ */
+ private void addUnmatched(ArrayMap<View, TransitionValues> unmatchedStart,
+ ArrayMap<View, TransitionValues> unmatchedEnd) {
+ // Views that only exist in the start Scene
+ for (int i = 0; i < unmatchedStart.size(); i++) {
+ final TransitionValues start = unmatchedStart.valueAt(i);
+ if (isValidTarget(start.view)) {
+ mStartValuesList.add(start);
+ mEndValuesList.add(null);
+ }
+ }
+
+ // Views that only exist in the end Scene
+ for (int i = 0; i < unmatchedEnd.size(); i++) {
+ final TransitionValues end = unmatchedEnd.valueAt(i);
+ if (isValidTarget(end.view)) {
+ mEndValuesList.add(end);
+ mStartValuesList.add(null);
+ }
+ }
+ }
+
+ private void matchStartAndEnd(TransitionValuesMaps startValues,
+ TransitionValuesMaps endValues) {
+ ArrayMap<View, TransitionValues> unmatchedStart = new ArrayMap<>(startValues.mViewValues);
+ ArrayMap<View, TransitionValues> unmatchedEnd = new ArrayMap<>(endValues.mViewValues);
+
+ for (int i = 0; i < mMatchOrder.length; i++) {
+ switch (mMatchOrder[i]) {
+ case MATCH_INSTANCE:
+ matchInstances(unmatchedStart, unmatchedEnd);
+ break;
+ case MATCH_NAME:
+ matchNames(unmatchedStart, unmatchedEnd,
+ startValues.mNameValues, endValues.mNameValues);
+ break;
+ case MATCH_ID:
+ matchIds(unmatchedStart, unmatchedEnd,
+ startValues.mIdValues, endValues.mIdValues);
+ break;
+ case MATCH_ITEM_ID:
+ matchItemIds(unmatchedStart, unmatchedEnd,
+ startValues.mItemIdValues, endValues.mItemIdValues);
+ break;
+ }
+ }
+ addUnmatched(unmatchedStart, unmatchedEnd);
+ }
+
+ /**
+ * This method, essentially a wrapper around all calls to createAnimator for all
+ * possible target views, is called with the entire set of start/end
+ * values. The implementation in Transition iterates through these lists
+ * and calls {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * with each set of start/end values on this transition. The
+ * TransitionSet subclass overrides this method and delegates it to
+ * each of its children in succession.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
+ TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
+ ArrayList<TransitionValues> endValuesList) {
+ if (DBG) {
+ Log.d(LOG_TAG, "createAnimators() for " + this);
+ }
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ long minStartDelay = Long.MAX_VALUE;
+ SparseIntArray startDelays = new SparseIntArray();
+ int startValuesListCount = startValuesList.size();
+ for (int i = 0; i < startValuesListCount; ++i) {
+ TransitionValues start = startValuesList.get(i);
+ TransitionValues end = endValuesList.get(i);
+ if (start != null && !start.mTargetedTransitions.contains(this)) {
+ start = null;
+ }
+ if (end != null && !end.mTargetedTransitions.contains(this)) {
+ end = null;
+ }
+ if (start == null && end == null) {
+ continue;
+ }
+ // Only bother trying to animate with values that differ between start/end
+ boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
+ if (isChanged) {
+ if (DBG) {
+ View view = (end != null) ? end.view : start.view;
+ Log.d(LOG_TAG, " differing start/end values for view " + view);
+ if (start == null || end == null) {
+ Log.d(LOG_TAG, " " + ((start == null)
+ ? "start null, end non-null" : "start non-null, end null"));
+ } else {
+ for (String key : start.values.keySet()) {
+ Object startValue = start.values.get(key);
+ Object endValue = end.values.get(key);
+ if (startValue != endValue && !startValue.equals(endValue)) {
+ Log.d(LOG_TAG, " " + key + ": start(" + startValue
+ + "), end(" + endValue + ")");
+ }
+ }
+ }
+ }
+ // TODO: what to do about targetIds and itemIds?
+ Animator animator = createAnimator(sceneRoot, start, end);
+ if (animator != null) {
+ // Save animation info for future cancellation purposes
+ View view;
+ TransitionValues infoValues = null;
+ if (end != null) {
+ view = end.view;
+ String[] properties = getTransitionProperties();
+ if (view != null && properties != null && properties.length > 0) {
+ infoValues = new TransitionValues();
+ infoValues.view = view;
+ TransitionValues newValues = endValues.mViewValues.get(view);
+ if (newValues != null) {
+ for (int j = 0; j < properties.length; ++j) {
+ infoValues.values.put(properties[j],
+ newValues.values.get(properties[j]));
+ }
+ }
+ int numExistingAnims = runningAnimators.size();
+ for (int j = 0; j < numExistingAnims; ++j) {
+ Animator anim = runningAnimators.keyAt(j);
+ AnimationInfo info = runningAnimators.get(anim);
+ if (info.mValues != null && info.mView == view
+ && info.mName.equals(getName())) {
+ if (info.mValues.equals(infoValues)) {
+ // Favor the old animator
+ animator = null;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ view = start.view;
+ }
+ if (animator != null) {
+ if (mPropagation != null) {
+ long delay = mPropagation.getStartDelay(sceneRoot, this, start, end);
+ startDelays.put(mAnimators.size(), (int) delay);
+ minStartDelay = Math.min(delay, minStartDelay);
+ }
+ AnimationInfo info = new AnimationInfo(view, getName(), this,
+ ViewUtils.getWindowId(sceneRoot), infoValues);
+ runningAnimators.put(animator, info);
+ mAnimators.add(animator);
+ }
+ }
+ }
+ }
+ if (minStartDelay != 0) {
+ for (int i = 0; i < startDelays.size(); i++) {
+ int index = startDelays.keyAt(i);
+ Animator animator = mAnimators.get(index);
+ long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
+ animator.setStartDelay(delay);
+ }
+ }
+ }
+
+ /**
+ * Internal utility method for checking whether a given view/id
+ * is valid for this transition, where "valid" means that either
+ * the Transition has no target/targetId list (the default, in which
+ * cause the transition should act on all views in the hiearchy), or
+ * the given view is in the target list or the view id is in the
+ * targetId list. If the target parameter is null, then the target list
+ * is not checked (this is in the case of ListView items, where the
+ * views are ignored and only the ids are used).
+ */
+ boolean isValidTarget(View target) {
+ int targetId = target.getId();
+ if (mTargetIdExcludes != null && mTargetIdExcludes.contains(targetId)) {
+ return false;
+ }
+ if (mTargetExcludes != null && mTargetExcludes.contains(target)) {
+ return false;
+ }
+ if (mTargetTypeExcludes != null) {
+ int numTypes = mTargetTypeExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ Class type = mTargetTypeExcludes.get(i);
+ if (type.isInstance(target)) {
+ return false;
+ }
+ }
+ }
+ if (mTargetNameExcludes != null && ViewCompat.getTransitionName(target) != null) {
+ if (mTargetNameExcludes.contains(ViewCompat.getTransitionName(target))) {
+ return false;
+ }
+ }
+ if (mTargetIds.size() == 0 && mTargets.size() == 0
+ && (mTargetTypes == null || mTargetTypes.isEmpty())
+ && (mTargetNames == null || mTargetNames.isEmpty())) {
+ return true;
+ }
+ if (mTargetIds.contains(targetId) || mTargets.contains(target)) {
+ return true;
+ }
+ if (mTargetNames != null && mTargetNames.contains(ViewCompat.getTransitionName(target))) {
+ return true;
+ }
+ if (mTargetTypes != null) {
+ for (int i = 0; i < mTargetTypes.size(); ++i) {
+ if (mTargetTypes.get(i).isInstance(target)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static ArrayMap<Animator, AnimationInfo> getRunningAnimators() {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = sRunningAnimators.get();
+ if (runningAnimators == null) {
+ runningAnimators = new ArrayMap<>();
+ sRunningAnimators.set(runningAnimators);
+ }
+ return runningAnimators;
+ }
+
+ /**
+ * This is called internally once all animations have been set up by the
+ * transition hierarchy. \
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void runAnimators() {
+ if (DBG) {
+ Log.d(LOG_TAG, "runAnimators() on " + this);
+ }
+ start();
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ // Now start every Animator that was previously created for this transition
+ for (Animator anim : mAnimators) {
+ if (DBG) {
+ Log.d(LOG_TAG, " anim: " + anim);
+ }
+ if (runningAnimators.containsKey(anim)) {
+ start();
+ runAnimator(anim, runningAnimators);
+ }
+ }
+ mAnimators.clear();
+ end();
+ }
+
+ private void runAnimator(Animator animator,
+ final ArrayMap<Animator, AnimationInfo> runningAnimators) {
+ if (animator != null) {
+ // TODO: could be a single listener instance for all of them since it uses the param
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCurrentAnimators.add(animation);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ runningAnimators.remove(animation);
+ mCurrentAnimators.remove(animation);
+ }
+ });
+ animate(animator);
+ }
+ }
+
+ /**
+ * Captures the values in the start scene for the properties that this
+ * transition monitors. These values are then passed as the startValues
+ * structure in a later call to
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
+ * The main concern for an implementation is what the
+ * properties are that the transition cares about and what the values are
+ * for all of those properties. The start and end values will be compared
+ * later during the
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * method to determine what, if any, animations, should be run.
+ *
+ * <p>Subclasses must implement this method. The method should only be called by the
+ * transition system; it is not intended to be called from external classes.</p>
+ *
+ * @param transitionValues The holder for any values that the Transition
+ * wishes to store. Values are stored in the <code>values</code> field
+ * of this TransitionValues object and are keyed from
+ * a String value. For example, to store a view's rotation value,
+ * a transition might call
+ * <code>transitionValues.values.put("appname:transitionname:rotation",
+ * view.getRotation())</code>. The target view will already be stored
+ * in
+ * the transitionValues structure when this method is called.
+ * @see #captureEndValues(TransitionValues)
+ * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
+ */
+ public abstract void captureStartValues(@NonNull TransitionValues transitionValues);
+
+ /**
+ * Captures the values in the end scene for the properties that this
+ * transition monitors. These values are then passed as the endValues
+ * structure in a later call to
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}.
+ * The main concern for an implementation is what the
+ * properties are that the transition cares about and what the values are
+ * for all of those properties. The start and end values will be compared
+ * later during the
+ * {@link #createAnimator(ViewGroup, TransitionValues, TransitionValues)}
+ * method to determine what, if any, animations, should be run.
+ *
+ * <p>Subclasses must implement this method. The method should only be called by the
+ * transition system; it is not intended to be called from external classes.</p>
+ *
+ * @param transitionValues The holder for any values that the Transition
+ * wishes to store. Values are stored in the <code>values</code> field
+ * of this TransitionValues object and are keyed from
+ * a String value. For example, to store a view's rotation value,
+ * a transition might call
+ * <code>transitionValues.values.put("appname:transitionname:rotation",
+ * view.getRotation())</code>. The target view will already be stored
+ * in
+ * the transitionValues structure when this method is called.
+ * @see #captureStartValues(TransitionValues)
+ * @see #createAnimator(ViewGroup, TransitionValues, TransitionValues)
+ */
+ public abstract void captureEndValues(@NonNull TransitionValues transitionValues);
+
+ /**
+ * Sets the target view instances that this Transition is interested in
+ * animating. By default, there are no targets, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targets constrains
+ * the Transition to only listen for, and act on, these views.
+ * All other views will be ignored.
+ *
+ * <p>The target list is like the {@link #addTarget(int) targetId}
+ * list except this list specifies the actual View instances, not the ids
+ * of the views. This is an important distinction when scene changes involve
+ * view hierarchies which have been inflated separately; different views may
+ * share the same id but not actually be the same instance. If the transition
+ * should treat those views as the same, then {@link #addTarget(int)} should be used
+ * instead of {@link #addTarget(View)}. If, on the other hand, scene changes involve
+ * changes all within the same view hierarchy, among views which do not
+ * necessarily have ids set on them, then the target list of views may be more
+ * convenient.</p>
+ *
+ * @param target A View on which the Transition will act, must be non-null.
+ * @return The Transition to which the target is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someView);</code>
+ * @see #addTarget(int)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull View target) {
+ mTargets.add(target);
+ return this;
+ }
+
+ /**
+ * Adds the id of a target view that this Transition is interested in
+ * animating. By default, there are no targetIds, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetIds constrains
+ * the Transition to only listen for, and act on, views with these IDs.
+ * Views with different IDs, or no IDs whatsoever, will be ignored.
+ *
+ * <p>Note that using ids to specify targets implies that ids should be unique
+ * within the view hierarchy underneath the scene root.</p>
+ *
+ * @param targetId The id of a target view, must be a positive number.
+ * @return The Transition to which the targetId is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someId);</code>
+ * @see View#getId()
+ */
+ @NonNull
+ public Transition addTarget(@IdRes int targetId) {
+ if (targetId != 0) {
+ mTargetIds.add(targetId);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the transitionName of a target view that this Transition is interested in
+ * animating. By default, there are no targetNames, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetNames constrains
+ * the Transition to only listen for, and act on, views with these transitionNames.
+ * Views with different transitionNames, or no transitionName whatsoever, will be ignored.
+ *
+ * <p>Note that transitionNames should be unique within the view hierarchy.</p>
+ *
+ * @param targetName The transitionName of a target view, must be non-null.
+ * @return The Transition to which the target transitionName is added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(someName);</code>
+ * @see ViewCompat#getTransitionName(View)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull String targetName) {
+ if (mTargetNames == null) {
+ mTargetNames = new ArrayList<>();
+ }
+ mTargetNames.add(targetName);
+ return this;
+ }
+
+ /**
+ * Adds the Class of a target view that this Transition is interested in
+ * animating. By default, there are no targetTypes, and a Transition will
+ * listen for changes on every view in the hierarchy below the sceneRoot
+ * of the Scene being transitioned into. Setting targetTypes constrains
+ * the Transition to only listen for, and act on, views with these classes.
+ * Views with different classes will be ignored.
+ *
+ * <p>Note that any View that can be cast to targetType will be included, so
+ * if targetType is <code>View.class</code>, all Views will be included.</p>
+ *
+ * @param targetType The type to include when running this transition.
+ * @return The Transition to which the target class was added.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);</code>
+ * @see #addTarget(int)
+ * @see #addTarget(android.view.View)
+ * @see #excludeTarget(Class, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition addTarget(@NonNull Class targetType) {
+ if (mTargetTypes == null) {
+ mTargetTypes = new ArrayList<>();
+ }
+ mTargetTypes.add(targetType);
+ return this;
+ }
+
+ /**
+ * Removes the given target from the list of targets that this Transition
+ * is interested in animating.
+ *
+ * @param target The target view, must be non-null.
+ * @return Transition The Transition from which the target is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTarget(someView);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull View target) {
+ mTargets.remove(target);
+ return this;
+ }
+
+ /**
+ * Removes the given targetId from the list of ids that this Transition
+ * is interested in animating.
+ *
+ * @param targetId The id of a target view, must be a positive number.
+ * @return The Transition from which the targetId is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTargetId(someId);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@IdRes int targetId) {
+ if (targetId != 0) {
+ mTargetIds.remove((Integer) targetId);
+ }
+ return this;
+ }
+
+ /**
+ * Removes the given targetName from the list of transitionNames that this Transition
+ * is interested in animating.
+ *
+ * @param targetName The transitionName of a target view, must not be null.
+ * @return The Transition from which the targetName is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTargetName(someName);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull String targetName) {
+ if (mTargetNames != null) {
+ mTargetNames.remove(targetName);
+ }
+ return this;
+ }
+
+ /**
+ * Removes the given target from the list of targets that this Transition
+ * is interested in animating.
+ *
+ * @param target The type of the target view, must be non-null.
+ * @return Transition The Transition from which the target is removed.
+ * Returning the same object makes it easier to chain calls during
+ * construction, such as
+ * <code>transitionSet.addTransitions(new Fade()).removeTarget(someType);</code>
+ */
+ @NonNull
+ public Transition removeTarget(@NonNull Class target) {
+ if (mTargetTypes != null) {
+ mTargetTypes.remove(target);
+ }
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private static <T> ArrayList<T> excludeObject(ArrayList<T> list, T target, boolean exclude) {
+ if (target != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, target);
+ } else {
+ list = ArrayListManager.remove(list, target);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Whether to add the given target to the list of targets to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param target The target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeChildren(View, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull View target, boolean exclude) {
+ mTargetExcludes = excludeView(mTargetExcludes, target, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given id to the list of target ids to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetId The id of a target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@IdRes int targetId, boolean exclude) {
+ mTargetIdExcludes = excludeId(mTargetIdExcludes, targetId, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given transitionName to the list of target transitionNames to exclude
+ * from this transition. The <code>exclude</code> parameter specifies whether the target
+ * should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded by their
+ * id, their instance reference, their transitionName, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetName The name of a target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
+ mTargetNameExcludes = excludeObject(mTargetNameExcludes, targetName, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the children of given target to the list of target children
+ * to exclude from this transition. The <code>exclude</code> parameter specifies
+ * whether the target should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param target The target to ignore when running this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded targets.
+ * @return This transition object.
+ * @see #excludeTarget(View, boolean)
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@NonNull View target, boolean exclude) {
+ mTargetChildExcludes = excludeView(mTargetChildExcludes, target, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the children of the given id to the list of targets to exclude
+ * from this transition. The <code>exclude</code> parameter specifies whether
+ * the children of the target should be added to or removed from the excluded list.
+ * Excluding children in this way provides a simple mechanism for excluding all
+ * children of specific targets, rather than individually excluding each
+ * child individually.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param targetId The id of a target whose children should be ignored when running
+ * this transition.
+ * @param exclude Whether to add the target to or remove the target from the
+ * current list of excluded-child targets.
+ * @return This transition object.
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeChildren(View, boolean)
+ * @see #excludeChildren(Class, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@IdRes int targetId, boolean exclude) {
+ mTargetIdChildExcludes = excludeId(mTargetIdChildExcludes, targetId, exclude);
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<Integer> excludeId(ArrayList<Integer> list, int targetId, boolean exclude) {
+ if (targetId > 0) {
+ if (exclude) {
+ list = ArrayListManager.add(list, targetId);
+ } else {
+ list = ArrayListManager.remove(list, targetId);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<View> excludeView(ArrayList<View> list, View target, boolean exclude) {
+ if (target != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, target);
+ } else {
+ list = ArrayListManager.remove(list, target);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Whether to add the given type to the list of types to exclude from this
+ * transition. The <code>exclude</code> parameter specifies whether the target
+ * type should be added to or removed from the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param type The type to ignore when running this transition.
+ * @param exclude Whether to add the target type to or remove it from the
+ * current list of excluded target types.
+ * @return This transition object.
+ * @see #excludeChildren(Class, boolean)
+ * @see #excludeTarget(int, boolean)
+ * @see #excludeTarget(View, boolean)
+ */
+ @NonNull
+ public Transition excludeTarget(@NonNull Class type, boolean exclude) {
+ mTargetTypeExcludes = excludeType(mTargetTypeExcludes, type, exclude);
+ return this;
+ }
+
+ /**
+ * Whether to add the given type to the list of types whose children should
+ * be excluded from this transition. The <code>exclude</code> parameter
+ * specifies whether the target type should be added to or removed from
+ * the excluded list.
+ *
+ * <p>Excluding targets is a general mechanism for allowing transitions to run on
+ * a view hierarchy while skipping target views that should not be part of
+ * the transition. For example, you may want to avoid animating children
+ * of a specific ListView or Spinner. Views can be excluded either by their
+ * id, or by their instance reference, or by the Class of that view
+ * (eg, {@link Spinner}).</p>
+ *
+ * @param type The type to ignore when running this transition.
+ * @param exclude Whether to add the target type to or remove it from the
+ * current list of excluded target types.
+ * @return This transition object.
+ * @see #excludeTarget(Class, boolean)
+ * @see #excludeChildren(int, boolean)
+ * @see #excludeChildren(View, boolean)
+ */
+ @NonNull
+ public Transition excludeChildren(@NonNull Class type, boolean exclude) {
+ mTargetTypeChildExcludes = excludeType(mTargetTypeChildExcludes, type, exclude);
+ return this;
+ }
+
+ /**
+ * Utility method to manage the boilerplate code that is the same whether we
+ * are excluding targets or their children.
+ */
+ private ArrayList<Class> excludeType(ArrayList<Class> list, Class type, boolean exclude) {
+ if (type != null) {
+ if (exclude) {
+ list = ArrayListManager.add(list, type);
+ } else {
+ list = ArrayListManager.remove(list, type);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Returns the array of target IDs that this transition limits itself to
+ * tracking and animating. If the array is null for both this method and
+ * {@link #getTargets()}, then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target IDs
+ */
+ @NonNull
+ public List<Integer> getTargetIds() {
+ return mTargetIds;
+ }
+
+ /**
+ * Returns the array of target views that this transition limits itself to
+ * tracking and animating. If the array is null for both this method and
+ * {@link #getTargetIds()}, then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target views
+ */
+ @NonNull
+ public List<View> getTargets() {
+ return mTargets;
+ }
+
+ /**
+ * Returns the list of target transitionNames that this transition limits itself to
+ * tracking and animating. If the list is null or empty for
+ * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
+ * {@link #getTargetTypes()} then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target transitionNames
+ */
+ @Nullable
+ public List<String> getTargetNames() {
+ return mTargetNames;
+ }
+
+ /**
+ * Returns the list of target transitionNames that this transition limits itself to
+ * tracking and animating. If the list is null or empty for
+ * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetNames()}, and
+ * {@link #getTargetTypes()} then this transition is
+ * not limited to specific views, and will handle changes to any views
+ * in the hierarchy of a scene change.
+ *
+ * @return the list of target Types
+ */
+ @Nullable
+ public List<Class> getTargetTypes() {
+ return mTargetTypes;
+ }
+
+ /**
+ * Recursive method that captures values for the given view and the
+ * hierarchy underneath it.
+ *
+ * @param sceneRoot The root of the view hierarchy being captured
+ * @param start true if this capture is happening before the scene change,
+ * false otherwise
+ */
+ void captureValues(ViewGroup sceneRoot, boolean start) {
+ clearValues(start);
+ if ((mTargetIds.size() > 0 || mTargets.size() > 0)
+ && (mTargetNames == null || mTargetNames.isEmpty())
+ && (mTargetTypes == null || mTargetTypes.isEmpty())) {
+ for (int i = 0; i < mTargetIds.size(); ++i) {
+ int id = mTargetIds.get(i);
+ View view = sceneRoot.findViewById(id);
+ if (view != null) {
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ }
+ for (int i = 0; i < mTargets.size(); ++i) {
+ View view = mTargets.get(i);
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ } else {
+ captureHierarchy(sceneRoot, start);
+ }
+ if (!start && mNameOverrides != null) {
+ int numOverrides = mNameOverrides.size();
+ ArrayList<View> overriddenViews = new ArrayList<>(numOverrides);
+ for (int i = 0; i < numOverrides; i++) {
+ String fromName = mNameOverrides.keyAt(i);
+ overriddenViews.add(mStartValues.mNameValues.remove(fromName));
+ }
+ for (int i = 0; i < numOverrides; i++) {
+ View view = overriddenViews.get(i);
+ if (view != null) {
+ String toName = mNameOverrides.valueAt(i);
+ mStartValues.mNameValues.put(toName, view);
+ }
+ }
+ }
+ }
+
+ private static void addViewValues(TransitionValuesMaps transitionValuesMaps,
+ View view, TransitionValues transitionValues) {
+ transitionValuesMaps.mViewValues.put(view, transitionValues);
+ int id = view.getId();
+ if (id >= 0) {
+ if (transitionValuesMaps.mIdValues.indexOfKey(id) >= 0) {
+ // Duplicate IDs cannot match by ID.
+ transitionValuesMaps.mIdValues.put(id, null);
+ } else {
+ transitionValuesMaps.mIdValues.put(id, view);
+ }
+ }
+ String name = ViewCompat.getTransitionName(view);
+ if (name != null) {
+ if (transitionValuesMaps.mNameValues.containsKey(name)) {
+ // Duplicate transitionNames: cannot match by transitionName.
+ transitionValuesMaps.mNameValues.put(name, null);
+ } else {
+ transitionValuesMaps.mNameValues.put(name, view);
+ }
+ }
+ if (view.getParent() instanceof ListView) {
+ ListView listview = (ListView) view.getParent();
+ if (listview.getAdapter().hasStableIds()) {
+ int position = listview.getPositionForView(view);
+ long itemId = listview.getItemIdAtPosition(position);
+ if (transitionValuesMaps.mItemIdValues.indexOfKey(itemId) >= 0) {
+ // Duplicate item IDs: cannot match by item ID.
+ View alreadyMatched = transitionValuesMaps.mItemIdValues.get(itemId);
+ if (alreadyMatched != null) {
+ ViewCompat.setHasTransientState(alreadyMatched, false);
+ transitionValuesMaps.mItemIdValues.put(itemId, null);
+ }
+ } else {
+ ViewCompat.setHasTransientState(view, true);
+ transitionValuesMaps.mItemIdValues.put(itemId, view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear valuesMaps for specified start/end state
+ *
+ * @param start true if the start values should be cleared, false otherwise
+ */
+ void clearValues(boolean start) {
+ if (start) {
+ mStartValues.mViewValues.clear();
+ mStartValues.mIdValues.clear();
+ mStartValues.mItemIdValues.clear();
+ } else {
+ mEndValues.mViewValues.clear();
+ mEndValues.mIdValues.clear();
+ mEndValues.mItemIdValues.clear();
+ }
+ }
+
+ /**
+ * Recursive method which captures values for an entire view hierarchy,
+ * starting at some root view. Transitions without targetIDs will use this
+ * method to capture values for all possible views.
+ *
+ * @param view The view for which to capture values. Children of this View
+ * will also be captured, recursively down to the leaf nodes.
+ * @param start true if values are being captured in the start scene, false
+ * otherwise.
+ */
+ private void captureHierarchy(View view, boolean start) {
+ if (view == null) {
+ return;
+ }
+ int id = view.getId();
+ if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) {
+ return;
+ }
+ if (mTargetExcludes != null && mTargetExcludes.contains(view)) {
+ return;
+ }
+ if (mTargetTypeExcludes != null) {
+ int numTypes = mTargetTypeExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ if (mTargetTypeExcludes.get(i).isInstance(view)) {
+ return;
+ }
+ }
+ }
+ if (view.getParent() instanceof ViewGroup) {
+ TransitionValues values = new TransitionValues();
+ values.view = view;
+ if (start) {
+ captureStartValues(values);
+ } else {
+ captureEndValues(values);
+ }
+ values.mTargetedTransitions.add(this);
+ capturePropagationValues(values);
+ if (start) {
+ addViewValues(mStartValues, view, values);
+ } else {
+ addViewValues(mEndValues, view, values);
+ }
+ }
+ if (view instanceof ViewGroup) {
+ // Don't traverse child hierarchy if there are any child-excludes on this view
+ if (mTargetIdChildExcludes != null && mTargetIdChildExcludes.contains(id)) {
+ return;
+ }
+ if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) {
+ return;
+ }
+ if (mTargetTypeChildExcludes != null) {
+ int numTypes = mTargetTypeChildExcludes.size();
+ for (int i = 0; i < numTypes; ++i) {
+ if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
+ return;
+ }
+ }
+ }
+ ViewGroup parent = (ViewGroup) view;
+ for (int i = 0; i < parent.getChildCount(); ++i) {
+ captureHierarchy(parent.getChildAt(i), start);
+ }
+ }
+ }
+
+ /**
+ * This method can be called by transitions to get the TransitionValues for
+ * any particular view during the transition-playing process. This might be
+ * necessary, for example, to query the before/after state of related views
+ * for a given transition.
+ */
+ @Nullable
+ public TransitionValues getTransitionValues(@NonNull View view, boolean start) {
+ if (mParent != null) {
+ return mParent.getTransitionValues(view, start);
+ }
+ TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues;
+ return valuesMaps.mViewValues.get(view);
+ }
+
+ /**
+ * Find the matched start or end value for a given View. This is only valid
+ * after playTransition starts. For example, it will be valid in
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)}, but not
+ * in {@link #captureStartValues(TransitionValues)}.
+ *
+ * @param view The view to find the match for.
+ * @param viewInStart Is View from the start values or end values.
+ * @return The matching TransitionValues for view in either start or end values, depending
+ * on viewInStart or null if there is no match for the given view.
+ */
+ TransitionValues getMatchedTransitionValues(View view, boolean viewInStart) {
+ if (mParent != null) {
+ return mParent.getMatchedTransitionValues(view, viewInStart);
+ }
+ ArrayList<TransitionValues> lookIn = viewInStart ? mStartValuesList : mEndValuesList;
+ if (lookIn == null) {
+ return null;
+ }
+ int count = lookIn.size();
+ int index = -1;
+ for (int i = 0; i < count; i++) {
+ TransitionValues values = lookIn.get(i);
+ if (values == null) {
+ return null;
+ }
+ if (values.view == view) {
+ index = i;
+ break;
+ }
+ }
+ TransitionValues values = null;
+ if (index >= 0) {
+ ArrayList<TransitionValues> matchIn = viewInStart ? mEndValuesList : mStartValuesList;
+ values = matchIn.get(index);
+ }
+ return values;
+ }
+
+ /**
+ * Pauses this transition, sending out calls to {@link
+ * TransitionListener#onTransitionPause(Transition)} to all listeners
+ * and pausing all running animators started by this transition.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void pause(View sceneRoot) {
+ if (!mEnded) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ AnimatorUtils.pause(anim);
+ }
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionPause(this);
+ }
+ }
+ mPaused = true;
+ }
+ }
+
+ /**
+ * Resumes this transition, sending out calls to {@link
+ * TransitionListener#onTransitionPause(Transition)} to all listeners
+ * and pausing all running animators started by this transition.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void resume(View sceneRoot) {
+ if (mPaused) {
+ if (!mEnded) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ AnimatorUtils.resume(anim);
+ }
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionResume(this);
+ }
+ }
+ }
+ mPaused = false;
+ }
+ }
+
+ /**
+ * Called by TransitionManager to play the transition. This calls
+ * createAnimators() to set things up and create all of the animations and then
+ * runAnimations() to actually start the animations.
+ */
+ void playTransition(ViewGroup sceneRoot) {
+ mStartValuesList = new ArrayList<>();
+ mEndValuesList = new ArrayList<>();
+ matchStartAndEnd(mStartValues, mEndValues);
+
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ Animator anim = runningAnimators.keyAt(i);
+ if (anim != null) {
+ AnimationInfo oldInfo = runningAnimators.get(anim);
+ if (oldInfo != null && oldInfo.mView != null
+ && windowId.equals(oldInfo.mWindowId)) {
+ TransitionValues oldValues = oldInfo.mValues;
+ View oldView = oldInfo.mView;
+ TransitionValues startValues = getTransitionValues(oldView, true);
+ TransitionValues endValues = getMatchedTransitionValues(oldView, true);
+ boolean cancel = (startValues != null || endValues != null)
+ && oldInfo.mTransition.isTransitionRequired(oldValues, endValues);
+ if (cancel) {
+ if (anim.isRunning() || anim.isStarted()) {
+ if (DBG) {
+ Log.d(LOG_TAG, "Canceling anim " + anim);
+ }
+ anim.cancel();
+ } else {
+ if (DBG) {
+ Log.d(LOG_TAG, "removing anim from info list: " + anim);
+ }
+ runningAnimators.remove(anim);
+ }
+ }
+ }
+ }
+ }
+
+ createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
+ runAnimators();
+ }
+
+ /**
+ * Returns whether or not the transition should create an Animator, based on the values
+ * captured during {@link #captureStartValues(TransitionValues)} and
+ * {@link #captureEndValues(TransitionValues)}. The default implementation compares the
+ * property values returned from {@link #getTransitionProperties()}, or all property values if
+ * {@code getTransitionProperties()} returns null. Subclasses may override this method to
+ * provide logic more specific to the transition implementation.
+ *
+ * @param startValues the values from captureStartValues, This may be {@code null} if the
+ * View did not exist in the start state.
+ * @param endValues the values from captureEndValues. This may be {@code null} if the View
+ * did not exist in the end state.
+ */
+ public boolean isTransitionRequired(@Nullable TransitionValues startValues,
+ @Nullable TransitionValues endValues) {
+ boolean valuesChanged = false;
+ // if startValues null, then transition didn't care to stash values,
+ // and won't get canceled
+ if (startValues != null && endValues != null) {
+ String[] properties = getTransitionProperties();
+ if (properties != null) {
+ for (String property : properties) {
+ if (isValueChanged(startValues, endValues, property)) {
+ valuesChanged = true;
+ break;
+ }
+ }
+ } else {
+ for (String key : startValues.values.keySet()) {
+ if (isValueChanged(startValues, endValues, key)) {
+ valuesChanged = true;
+ break;
+ }
+ }
+ }
+ }
+ return valuesChanged;
+ }
+
+ private static boolean isValueChanged(TransitionValues oldValues, TransitionValues newValues,
+ String key) {
+ Object oldValue = oldValues.values.get(key);
+ Object newValue = newValues.values.get(key);
+ boolean changed;
+ if (oldValue == null && newValue == null) {
+ // both are null
+ changed = false;
+ } else if (oldValue == null || newValue == null) {
+ // one is null
+ changed = true;
+ } else {
+ // neither is null
+ changed = !oldValue.equals(newValue);
+ }
+ if (DBG && changed) {
+ Log.d(LOG_TAG, "Transition.playTransition: "
+ + "oldValue != newValue for " + key
+ + ": old, new = " + oldValue + ", " + newValue);
+ }
+ return changed;
+ }
+
+ /**
+ * This is a utility method used by subclasses to handle standard parts of
+ * setting up and running an Animator: it sets the {@link #getDuration()
+ * duration} and the {@link #getStartDelay() startDelay}, starts the
+ * animation, and, when the animator ends, calls {@link #end()}.
+ *
+ * @param animator The Animator to be run during this transition.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void animate(Animator animator) {
+ // TODO: maybe pass auto-end as a boolean parameter?
+ if (animator == null) {
+ end();
+ } else {
+ if (getDuration() >= 0) {
+ animator.setDuration(getDuration());
+ }
+ if (getStartDelay() >= 0) {
+ animator.setStartDelay(getStartDelay());
+ }
+ if (getInterpolator() != null) {
+ animator.setInterpolator(getInterpolator());
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ end();
+ animation.removeListener(this);
+ }
+ });
+ animator.start();
+ }
+ }
+
+ /**
+ * This method is called automatically by the transition and
+ * TransitionSet classes prior to a Transition subclass starting;
+ * subclasses should not need to call it directly.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void start() {
+ if (mNumInstances == 0) {
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionStart(this);
+ }
+ }
+ mEnded = false;
+ }
+ mNumInstances++;
+ }
+
+ /**
+ * This method is called automatically by the Transition and
+ * TransitionSet classes when a transition finishes, either because
+ * a transition did nothing (returned a null Animator from
+ * {@link Transition#createAnimator(ViewGroup, TransitionValues,
+ * TransitionValues)}) or because the transition returned a valid
+ * Animator and end() was called in the onAnimationEnd()
+ * callback of the AnimatorListener.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void end() {
+ --mNumInstances;
+ if (mNumInstances == 0) {
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionEnd(this);
+ }
+ }
+ for (int i = 0; i < mStartValues.mItemIdValues.size(); ++i) {
+ View view = mStartValues.mItemIdValues.valueAt(i);
+ if (view != null) {
+ ViewCompat.setHasTransientState(view, false);
+ }
+ }
+ for (int i = 0; i < mEndValues.mItemIdValues.size(); ++i) {
+ View view = mEndValues.mItemIdValues.valueAt(i);
+ if (view != null) {
+ ViewCompat.setHasTransientState(view, false);
+ }
+ }
+ mEnded = true;
+ }
+ }
+
+ /**
+ * Force the transition to move to its end state, ending all the animators.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ void forceToEnd(ViewGroup sceneRoot) {
+ ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ int numOldAnims = runningAnimators.size();
+ if (sceneRoot != null) {
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = runningAnimators.valueAt(i);
+ if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
+ Animator anim = runningAnimators.keyAt(i);
+ anim.end();
+ }
+ }
+ }
+ }
+
+ /**
+ * This method cancels a transition that is currently running.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected void cancel() {
+ int numAnimators = mCurrentAnimators.size();
+ for (int i = numAnimators - 1; i >= 0; i--) {
+ Animator animator = mCurrentAnimators.get(i);
+ animator.cancel();
+ }
+ if (mListeners != null && mListeners.size() > 0) {
+ @SuppressWarnings("unchecked") ArrayList<TransitionListener> tmpListeners =
+ (ArrayList<TransitionListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onTransitionCancel(this);
+ }
+ }
+ }
+
+ /**
+ * Adds a listener to the set of listeners that are sent events through the
+ * life of an animation, such as start, repeat, and end.
+ *
+ * @param listener the listener to be added to the current set of listeners
+ * for this animation.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition addListener(@NonNull TransitionListener listener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ }
+ mListeners.add(listener);
+ return this;
+ }
+
+ /**
+ * Removes a listener from the set listening to this animation.
+ *
+ * @param listener the listener to be removed from the current set of
+ * listeners for this transition.
+ * @return This transition object.
+ */
+ @NonNull
+ public Transition removeListener(@NonNull TransitionListener listener) {
+ if (mListeners == null) {
+ return this;
+ }
+ mListeners.remove(listener);
+ if (mListeners.size() == 0) {
+ mListeners = null;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the algorithm used to calculate two-dimensional interpolation.
+ * <p>
+ * Transitions such as {@link android.transition.ChangeBounds} move Views, typically
+ * in a straight path between the start and end positions. Applications that desire to
+ * have these motions move in a curve can change how Views interpolate in two dimensions
+ * by extending PathMotion and implementing
+ * {@link android.transition.PathMotion#getPath(float, float, float, float)}.
+ * </p>
+ *
+ * @param pathMotion Algorithm object to use for determining how to interpolate in two
+ * dimensions. If null, a straight-path algorithm will be used.
+ * @see android.transition.ArcMotion
+ * @see PatternPathMotion
+ * @see android.transition.PathMotion
+ */
+ public void setPathMotion(@Nullable PathMotion pathMotion) {
+ if (pathMotion == null) {
+ mPathMotion = STRAIGHT_PATH_MOTION;
+ } else {
+ mPathMotion = pathMotion;
+ }
+ }
+
+ /**
+ * Returns the algorithm object used to interpolate along two dimensions. This is typically
+ * used to determine the View motion between two points.
+ *
+ * @return The algorithm object used to interpolate along two dimensions.
+ * @see android.transition.ArcMotion
+ * @see PatternPathMotion
+ * @see android.transition.PathMotion
+ */
+ @NonNull
+ public PathMotion getPathMotion() {
+ return mPathMotion;
+ }
+
+ /**
+ * Sets the callback to use to find the epicenter of a Transition. A null value indicates
+ * that there is no epicenter in the Transition and onGetEpicenter() will return null.
+ * Transitions like {@link android.transition.Explode} use a point or Rect to orient
+ * the direction of travel. This is called the epicenter of the Transition and is
+ * typically centered on a touched View. The
+ * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
+ * dynamically retrieve the epicenter during a Transition.
+ *
+ * @param epicenterCallback The callback to use to find the epicenter of the Transition.
+ */
+ public void setEpicenterCallback(@Nullable EpicenterCallback epicenterCallback) {
+ mEpicenterCallback = epicenterCallback;
+ }
+
+ /**
+ * Returns the callback used to find the epicenter of the Transition.
+ * Transitions like {@link android.transition.Explode} use a point or Rect to orient
+ * the direction of travel. This is called the epicenter of the Transition and is
+ * typically centered on a touched View. The
+ * {@link android.transition.Transition.EpicenterCallback} allows a Transition to
+ * dynamically retrieve the epicenter during a Transition.
+ *
+ * @return the callback used to find the epicenter of the Transition.
+ */
+ @Nullable
+ public EpicenterCallback getEpicenterCallback() {
+ return mEpicenterCallback;
+ }
+
+ /**
+ * Returns the epicenter as specified by the
+ * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
+ *
+ * @return the epicenter as specified by the
+ * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists.
+ * @see #setEpicenterCallback(EpicenterCallback)
+ */
+ @Nullable
+ public Rect getEpicenter() {
+ if (mEpicenterCallback == null) {
+ return null;
+ }
+ return mEpicenterCallback.onGetEpicenter(this);
+ }
+
+ /**
+ * Sets the method for determining Animator start delays.
+ * When a Transition affects several Views like {@link android.transition.Explode} or
+ * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
+ * such that the Animator start delay depends on position of the View. The
+ * TransitionPropagation specifies how the start delays are calculated.
+ *
+ * @param transitionPropagation The class used to determine the start delay of
+ * Animators created by this Transition. A null value
+ * indicates that no delay should be used.
+ */
+ public void setPropagation(@Nullable TransitionPropagation transitionPropagation) {
+ mPropagation = transitionPropagation;
+ }
+
+ /**
+ * Returns the {@link android.transition.TransitionPropagation} used to calculate Animator
+ * start
+ * delays.
+ * When a Transition affects several Views like {@link android.transition.Explode} or
+ * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect
+ * such that the Animator start delay depends on position of the View. The
+ * TransitionPropagation specifies how the start delays are calculated.
+ *
+ * @return the {@link android.transition.TransitionPropagation} used to calculate Animator start
+ * delays. This is null by default.
+ */
+ @Nullable
+ public TransitionPropagation getPropagation() {
+ return mPropagation;
+ }
+
+ /**
+ * Captures TransitionPropagation values for the given view and the
+ * hierarchy underneath it.
+ */
+ void capturePropagationValues(TransitionValues transitionValues) {
+ if (mPropagation != null && !transitionValues.values.isEmpty()) {
+ String[] propertyNames = mPropagation.getPropagationProperties();
+ if (propertyNames == null) {
+ return;
+ }
+ boolean containsAll = true;
+ for (int i = 0; i < propertyNames.length; i++) {
+ if (!transitionValues.values.containsKey(propertyNames[i])) {
+ containsAll = false;
+ break;
+ }
+ }
+ if (!containsAll) {
+ mPropagation.captureValues(transitionValues);
+ }
+ }
+ }
+
+ Transition setSceneRoot(ViewGroup sceneRoot) {
+ mSceneRoot = sceneRoot;
+ return this;
+ }
+
+ void setCanRemoveViews(boolean canRemoveViews) {
+ mCanRemoveViews = canRemoveViews;
+ }
+
+ @Override
+ public String toString() {
+ return toString("");
+ }
+
+ @Override
+ public Transition clone() {
+ try {
+ Transition clone = (Transition) super.clone();
+ clone.mAnimators = new ArrayList<>();
+ clone.mStartValues = new TransitionValuesMaps();
+ clone.mEndValues = new TransitionValuesMaps();
+ clone.mStartValuesList = null;
+ clone.mEndValuesList = null;
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the name of this Transition. This name is used internally to distinguish
+ * between different transitions to determine when interrupting transitions overlap.
+ * For example, a ChangeBounds running on the same target view as another ChangeBounds
+ * should determine whether the old transition is animating to different end values
+ * and should be canceled in favor of the new transition.
+ *
+ * <p>By default, a Transition's name is simply the value of {@link Class#getName()},
+ * but subclasses are free to override and return something different.</p>
+ *
+ * @return The name of this transition.
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ String toString(String indent) {
+ String result = indent + getClass().getSimpleName() + "@"
+ + Integer.toHexString(hashCode()) + ": ";
+ if (mDuration != -1) {
+ result += "dur(" + mDuration + ") ";
+ }
+ if (mStartDelay != -1) {
+ result += "dly(" + mStartDelay + ") ";
+ }
+ if (mInterpolator != null) {
+ result += "interp(" + mInterpolator + ") ";
+ }
+ if (mTargetIds.size() > 0 || mTargets.size() > 0) {
+ result += "tgts(";
+ if (mTargetIds.size() > 0) {
+ for (int i = 0; i < mTargetIds.size(); ++i) {
+ if (i > 0) {
+ result += ", ";
+ }
+ result += mTargetIds.get(i);
+ }
+ }
+ if (mTargets.size() > 0) {
+ for (int i = 0; i < mTargets.size(); ++i) {
+ if (i > 0) {
+ result += ", ";
+ }
+ result += mTargets.get(i);
+ }
+ }
+ result += ")";
+ }
+ return result;
+ }
+
+ /**
+ * A transition listener receives notifications from a transition.
+ * Notifications indicate transition lifecycle events.
+ */
+ public interface TransitionListener {
+
+ /**
+ * Notification about the start of the transition.
+ *
+ * @param transition The started transition.
+ */
+ void onTransitionStart(@NonNull Transition transition);
+
+ /**
+ * Notification about the end of the transition. Canceled transitions
+ * will always notify listeners of both the cancellation and end
+ * events. That is, {@link #onTransitionEnd(Transition)} is always called,
+ * regardless of whether the transition was canceled or played
+ * through to completion.
+ *
+ * @param transition The transition which reached its end.
+ */
+ void onTransitionEnd(@NonNull Transition transition);
+
+ /**
+ * Notification about the cancellation of the transition.
+ * Note that cancel may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state on target objects which was set at
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
+ * createAnimator()} time.
+ *
+ * @param transition The transition which was canceled.
+ */
+ void onTransitionCancel(@NonNull Transition transition);
+
+ /**
+ * Notification when a transition is paused.
+ * Note that createAnimator() may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state on target objects which was set at
+ * {@link #createAnimator(android.view.ViewGroup, TransitionValues, TransitionValues)
+ * createAnimator()} time.
+ *
+ * @param transition The transition which was paused.
+ */
+ void onTransitionPause(@NonNull Transition transition);
+
+ /**
+ * Notification when a transition is resumed.
+ * Note that resume() may be called by a parent {@link TransitionSet} on
+ * a child transition which has not yet started. This allows the child
+ * transition to restore state which may have changed in an earlier call
+ * to {@link #onTransitionPause(Transition)}.
+ *
+ * @param transition The transition which was resumed.
+ */
+ void onTransitionResume(@NonNull Transition transition);
+ }
+
+ /**
+ * Holds information about each animator used when a new transition starts
+ * while other transitions are still running to determine whether a running
+ * animation should be canceled or a new animation noop'd. The structure holds
+ * information about the state that an animation is going to, to be compared to
+ * end state of a new animation.
+ */
+ private static class AnimationInfo {
+
+ View mView;
+
+ String mName;
+
+ TransitionValues mValues;
+
+ WindowIdImpl mWindowId;
+
+ Transition mTransition;
+
+ AnimationInfo(View view, String name, Transition transition, WindowIdImpl windowId,
+ TransitionValues values) {
+ mView = view;
+ mName = name;
+ mValues = values;
+ mWindowId = windowId;
+ mTransition = transition;
+ }
+ }
+
+ /**
+ * Utility class for managing typed ArrayLists efficiently. In particular, this
+ * can be useful for lists that we don't expect to be used often (eg, the exclude
+ * lists), so we'd like to keep them nulled out by default. This causes the code to
+ * become tedious, with constant null checks, code to allocate when necessary,
+ * and code to null out the reference when the list is empty. This class encapsulates
+ * all of that functionality into simple add()/remove() methods which perform the
+ * necessary checks, allocation/null-out as appropriate, and return the
+ * resulting list.
+ */
+ private static class ArrayListManager {
+
+ /**
+ * Add the specified item to the list, returning the resulting list.
+ * The returned list can either the be same list passed in or, if that
+ * list was null, the new list that was created.
+ *
+ * Note that the list holds unique items; if the item already exists in the
+ * list, the list is not modified.
+ */
+ static <T> ArrayList<T> add(ArrayList<T> list, T item) {
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ if (!list.contains(item)) {
+ list.add(item);
+ }
+ return list;
+ }
+
+ /**
+ * Remove the specified item from the list, returning the resulting list.
+ * The returned list can either the be same list passed in or, if that
+ * list becomes empty as a result of the remove(), the new list was created.
+ */
+ static <T> ArrayList<T> remove(ArrayList<T> list, T item) {
+ if (list != null) {
+ list.remove(item);
+ if (list.isEmpty()) {
+ list = null;
+ }
+ }
+ return list;
+ }
+ }
+
+ /**
+ * Class to get the epicenter of Transition. Use
+ * {@link #setEpicenterCallback(EpicenterCallback)} to set the callback used to calculate the
+ * epicenter of the Transition. Override {@link #getEpicenter()} to return the rectangular
+ * region in screen coordinates of the epicenter of the transition.
+ *
+ * @see #setEpicenterCallback(EpicenterCallback)
+ */
+ public abstract static class EpicenterCallback {
+
+ /**
+ * Implementers must override to return the epicenter of the Transition in screen
+ * coordinates. Transitions like {@link android.transition.Explode} depend upon
+ * an epicenter for the Transition. In Explode, Views move toward or away from the
+ * center of the epicenter Rect along the vector between the epicenter and the center
+ * of the View appearing and disappearing. Some Transitions, such as
+ * {@link android.transition.Fade} pay no attention to the epicenter.
+ *
+ * @param transition The transition for which the epicenter applies.
+ * @return The Rect region of the epicenter of <code>transition</code> or null if
+ * there is no epicenter.
+ */
+ public abstract Rect onGetEpicenter(@NonNull Transition transition);
+ }
+
+}
diff --git a/transition/src/android/support/transition/TransitionInflater.java b/transition/src/main/java/android/support/transition/TransitionInflater.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionInflater.java
rename to transition/src/main/java/android/support/transition/TransitionInflater.java
diff --git a/transition/src/android/support/transition/TransitionListenerAdapter.java b/transition/src/main/java/android/support/transition/TransitionListenerAdapter.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionListenerAdapter.java
rename to transition/src/main/java/android/support/transition/TransitionListenerAdapter.java
diff --git a/transition/src/android/support/transition/TransitionManager.java b/transition/src/main/java/android/support/transition/TransitionManager.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionManager.java
rename to transition/src/main/java/android/support/transition/TransitionManager.java
diff --git a/transition/src/android/support/transition/TransitionPropagation.java b/transition/src/main/java/android/support/transition/TransitionPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionPropagation.java
rename to transition/src/main/java/android/support/transition/TransitionPropagation.java
diff --git a/transition/src/android/support/transition/TransitionSet.java b/transition/src/main/java/android/support/transition/TransitionSet.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionSet.java
rename to transition/src/main/java/android/support/transition/TransitionSet.java
diff --git a/transition/src/android/support/transition/TransitionUtils.java b/transition/src/main/java/android/support/transition/TransitionUtils.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionUtils.java
rename to transition/src/main/java/android/support/transition/TransitionUtils.java
diff --git a/transition/src/android/support/transition/TransitionValues.java b/transition/src/main/java/android/support/transition/TransitionValues.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionValues.java
rename to transition/src/main/java/android/support/transition/TransitionValues.java
diff --git a/transition/src/android/support/transition/TransitionValuesMaps.java b/transition/src/main/java/android/support/transition/TransitionValuesMaps.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionValuesMaps.java
rename to transition/src/main/java/android/support/transition/TransitionValuesMaps.java
diff --git a/transition/src/android/support/transition/TranslationAnimationCreator.java b/transition/src/main/java/android/support/transition/TranslationAnimationCreator.java
similarity index 100%
rename from transition/src/android/support/transition/TranslationAnimationCreator.java
rename to transition/src/main/java/android/support/transition/TranslationAnimationCreator.java
diff --git a/transition/api14/android/support/transition/ViewGroupOverlayApi14.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewGroupOverlayApi14.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayApi14.java
diff --git a/transition/api18/android/support/transition/ViewGroupOverlayApi18.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewGroupOverlayApi18.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayApi18.java
diff --git a/transition/base/android/support/transition/ViewGroupOverlayImpl.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewGroupOverlayImpl.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayImpl.java
diff --git a/transition/src/android/support/transition/ViewGroupUtils.java b/transition/src/main/java/android/support/transition/ViewGroupUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ViewGroupUtils.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtils.java
diff --git a/transition/api14/android/support/transition/ViewGroupUtilsApi14.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewGroupUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsApi14.java
diff --git a/transition/api18/android/support/transition/ViewGroupUtilsApi18.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewGroupUtilsApi18.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsApi18.java
diff --git a/transition/base/android/support/transition/ViewGroupUtilsImpl.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewGroupUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsImpl.java
diff --git a/transition/api14/android/support/transition/ViewOverlayApi14.java b/transition/src/main/java/android/support/transition/ViewOverlayApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewOverlayApi14.java
rename to transition/src/main/java/android/support/transition/ViewOverlayApi14.java
diff --git a/transition/api18/android/support/transition/ViewOverlayApi18.java b/transition/src/main/java/android/support/transition/ViewOverlayApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewOverlayApi18.java
rename to transition/src/main/java/android/support/transition/ViewOverlayApi18.java
diff --git a/transition/base/android/support/transition/ViewOverlayImpl.java b/transition/src/main/java/android/support/transition/ViewOverlayImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewOverlayImpl.java
rename to transition/src/main/java/android/support/transition/ViewOverlayImpl.java
diff --git a/transition/src/android/support/transition/ViewUtils.java b/transition/src/main/java/android/support/transition/ViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ViewUtils.java
rename to transition/src/main/java/android/support/transition/ViewUtils.java
diff --git a/transition/api14/android/support/transition/ViewUtilsApi14.java b/transition/src/main/java/android/support/transition/ViewUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi14.java
diff --git a/transition/api18/android/support/transition/ViewUtilsApi18.java b/transition/src/main/java/android/support/transition/ViewUtilsApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewUtilsApi18.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi18.java
diff --git a/transition/api19/android/support/transition/ViewUtilsApi19.java b/transition/src/main/java/android/support/transition/ViewUtilsApi19.java
similarity index 100%
rename from transition/api19/android/support/transition/ViewUtilsApi19.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi19.java
diff --git a/transition/api21/android/support/transition/ViewUtilsApi21.java b/transition/src/main/java/android/support/transition/ViewUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ViewUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi21.java
diff --git a/transition/api22/android/support/transition/ViewUtilsApi22.java b/transition/src/main/java/android/support/transition/ViewUtilsApi22.java
similarity index 100%
rename from transition/api22/android/support/transition/ViewUtilsApi22.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi22.java
diff --git a/transition/base/android/support/transition/ViewUtilsImpl.java b/transition/src/main/java/android/support/transition/ViewUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ViewUtilsImpl.java
diff --git a/transition/src/android/support/transition/Visibility.java b/transition/src/main/java/android/support/transition/Visibility.java
similarity index 100%
rename from transition/src/android/support/transition/Visibility.java
rename to transition/src/main/java/android/support/transition/Visibility.java
diff --git a/transition/src/android/support/transition/VisibilityPropagation.java b/transition/src/main/java/android/support/transition/VisibilityPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/VisibilityPropagation.java
rename to transition/src/main/java/android/support/transition/VisibilityPropagation.java
diff --git a/transition/api14/android/support/transition/WindowIdApi14.java b/transition/src/main/java/android/support/transition/WindowIdApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/WindowIdApi14.java
rename to transition/src/main/java/android/support/transition/WindowIdApi14.java
diff --git a/transition/api18/android/support/transition/WindowIdApi18.java b/transition/src/main/java/android/support/transition/WindowIdApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/WindowIdApi18.java
rename to transition/src/main/java/android/support/transition/WindowIdApi18.java
diff --git a/transition/base/android/support/transition/WindowIdImpl.java b/transition/src/main/java/android/support/transition/WindowIdImpl.java
similarity index 100%
rename from transition/base/android/support/transition/WindowIdImpl.java
rename to transition/src/main/java/android/support/transition/WindowIdImpl.java
diff --git a/transition/src/main/java/android/support/transition/package.html b/transition/src/main/java/android/support/transition/package.html
new file mode 100644
index 0000000..d8394a5
--- /dev/null
+++ b/transition/src/main/java/android/support/transition/package.html
@@ -0,0 +1,8 @@
+<body>
+
+Support android.transition classes to provide transition API back to Android API level 14.
+This library contains {@link android.support.transition.Transition},
+{@link android.support.transition.TransitionManager}, and other related classes
+back-ported from their platform versions introduced Android API level 19.
+
+</body>
diff --git a/tv-provider/api/current.txt b/tv-provider/api/current.txt
index 42cad9f..80421e9 100644
--- a/tv-provider/api/current.txt
+++ b/tv-provider/api/current.txt
@@ -531,6 +531,7 @@
method public int getWatchNextType();
method public android.content.ContentValues toContentValues();
method public java.lang.String toString();
+ field public static final int WATCH_NEXT_TYPE_UNKNOWN = -1; // 0xffffffff
}
public static final class WatchNextProgram.Builder {
diff --git a/tv-provider/lint-baseline.xml b/tv-provider/lint-baseline.xml
index 9814796..4387a5a 100644
--- a/tv-provider/lint-baseline.xml
+++ b/tv-provider/lint-baseline.xml
@@ -1,92 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-beta6">
-
- <issue
- id="WrongConstant"
- message="Must be one of: PreviewProgramColumns.TYPE_MOVIE, PreviewProgramColumns.TYPE_TV_SERIES, PreviewProgramColumns.TYPE_TV_SEASON, PreviewProgramColumns.TYPE_TV_EPISODE, PreviewProgramColumns.TYPE_CLIP, PreviewProgramColumns.TYPE_EVENT, PreviewProgramColumns.TYPE_CHANNEL, PreviewProgramColumns.TYPE_TRACK, PreviewProgramColumns.TYPE_ALBUM, PreviewProgramColumns.TYPE_ARTIST, PreviewProgramColumns.TYPE_PLAYLIST, PreviewProgramColumns.TYPE_STATION, PreviewProgramColumns.TYPE_GAME"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
- line="130"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
- line="140"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
- line="150"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: PreviewProgramColumns.AVAILABILITY_AVAILABLE, PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION, PreviewProgramColumns.AVAILABILITY_PAID_CONTENT, PreviewProgramColumns.AVAILABILITY_PURCHASED, PreviewProgramColumns.AVAILABILITY_FREE"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
- line="168"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: PreviewProgramColumns.INTERACTION_TYPE_VIEWS, PreviewProgramColumns.INTERACTION_TYPE_LISTENS, PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS, PreviewProgramColumns.INTERACTION_TYPE_FANS, PreviewProgramColumns.INTERACTION_TYPE_LIKES, PreviewProgramColumns.INTERACTION_TYPE_THUMBS, PreviewProgramColumns.INTERACTION_TYPE_VIEWERS"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
- line="219"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: ProgramColumns.REVIEW_RATING_STYLE_STARS, ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN, ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/BaseProgram.java"
- line="257"
- column="28"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: Genres.FAMILY_KIDS, Genres.SPORTS, Genres.SHOPPING, Genres.MOVIES, Genres.COMEDY, Genres.TRAVEL, Genres.DRAMA, Genres.EDUCATION, Genres.ANIMAL_WILDLIFE, Genres.NEWS, Genres.GAMING, Genres.ARTS, Genres.ENTERTAINMENT, Genres.LIFE_STYLE, Genres.MUSIC, Genres.PREMIER, Genres.TECH_SCIENCE"
- errorLine1=" mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/Program.java"
- line="286"
- column="81"/>
- </issue>
-
- <issue
- id="WrongConstant"
- message="Must be one of: WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE, WatchNextPrograms.WATCH_NEXT_TYPE_NEXT, WatchNextPrograms.WATCH_NEXT_TYPE_NEW, WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST"
- errorLine1=" return i == null ? INVALID_INT_VALUE : i;"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/android/support/media/tv/WatchNextProgram.java"
- line="99"
- column="28"/>
- </issue>
+<issues format="4" by="lint 3.0.0-beta7">
</issues>
diff --git a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
index 1423d9d..39c3014 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
@@ -23,14 +23,13 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.IntDef;
import android.support.annotation.RestrictTo;
import android.support.media.tv.TvContractCompat.PreviewProgramColumns;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Availability;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type;
import android.support.media.tv.TvContractCompat.PreviewPrograms;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -55,6 +54,89 @@
private static final int IS_LIVE = 1;
private static final int IS_BROWSABLE = 1;
+ /** @hide */
+ @IntDef({
+ TYPE_UNKNOWN,
+ PreviewProgramColumns.TYPE_MOVIE,
+ PreviewProgramColumns.TYPE_TV_SERIES,
+ PreviewProgramColumns.TYPE_TV_SEASON,
+ PreviewProgramColumns.TYPE_TV_EPISODE,
+ PreviewProgramColumns.TYPE_CLIP,
+ PreviewProgramColumns.TYPE_EVENT,
+ PreviewProgramColumns.TYPE_CHANNEL,
+ PreviewProgramColumns.TYPE_TRACK,
+ PreviewProgramColumns.TYPE_ALBUM,
+ PreviewProgramColumns.TYPE_ARTIST,
+ PreviewProgramColumns.TYPE_PLAYLIST,
+ PreviewProgramColumns.TYPE_STATION,
+ PreviewProgramColumns.TYPE_GAME
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface Type {}
+
+ /**
+ * The unknown program type.
+ */
+ private static final int TYPE_UNKNOWN = -1;
+
+ /** @hide */
+ @IntDef({
+ ASPECT_RATIO_UNKNOWN,
+ PreviewProgramColumns.ASPECT_RATIO_16_9,
+ PreviewProgramColumns.ASPECT_RATIO_3_2,
+ PreviewProgramColumns.ASPECT_RATIO_4_3,
+ PreviewProgramColumns.ASPECT_RATIO_1_1,
+ PreviewProgramColumns.ASPECT_RATIO_2_3,
+ PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface AspectRatio {}
+
+ /**
+ * The aspect ratio for unknown aspect ratios.
+ */
+ private static final int ASPECT_RATIO_UNKNOWN = -1;
+
+ /** @hide */
+ @IntDef({
+ AVAILABILITY_UNKNOWN,
+ PreviewProgramColumns.AVAILABILITY_AVAILABLE,
+ PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION,
+ PreviewProgramColumns.AVAILABILITY_PAID_CONTENT,
+ PreviewProgramColumns.AVAILABILITY_PURCHASED,
+ PreviewProgramColumns.AVAILABILITY_FREE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface Availability {}
+
+ /**
+ * The unknown availability.
+ */
+ private static final int AVAILABILITY_UNKNOWN = -1;
+
+ /** @hide */
+ @IntDef({
+ INTERACTION_TYPE_UNKNOWN,
+ PreviewProgramColumns.INTERACTION_TYPE_VIEWS,
+ PreviewProgramColumns.INTERACTION_TYPE_LISTENS,
+ PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS,
+ PreviewProgramColumns.INTERACTION_TYPE_FANS,
+ PreviewProgramColumns.INTERACTION_TYPE_LIKES,
+ PreviewProgramColumns.INTERACTION_TYPE_THUMBS,
+ PreviewProgramColumns.INTERACTION_TYPE_VIEWERS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface InteractionType {}
+
+ /**
+ * The unknown interaction type.
+ */
+ private static final int INTERACTION_TYPE_UNKNOWN = -1;
+
BasePreviewProgram(Builder builder) {
super(builder);
}
@@ -127,7 +209,7 @@
*/
public @Type int getType() {
Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? TYPE_UNKNOWN : i;
}
/**
@@ -137,7 +219,7 @@
*/
public @AspectRatio int getPosterArtAspectRatio() {
Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? ASPECT_RATIO_UNKNOWN : i;
}
/**
@@ -147,7 +229,7 @@
*/
public @AspectRatio int getThumbnailAspectRatio() {
Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? ASPECT_RATIO_UNKNOWN : i;
}
/**
@@ -165,7 +247,7 @@
*/
public @Availability int getAvailability() {
Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? AVAILABILITY_UNKNOWN : i;
}
/**
@@ -216,7 +298,7 @@
*/
public @InteractionType int getInteractionType() {
Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? INTERACTION_TYPE_UNKNOWN : i;
}
/**
diff --git a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
index e4ce9d1..23b5cf9 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
@@ -22,13 +22,16 @@
import android.media.tv.TvContentRating;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.IntDef;
import android.support.annotation.RestrictTo;
import android.support.media.tv.TvContractCompat.BaseTvColumns;
import android.support.media.tv.TvContractCompat.ProgramColumns;
-import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle;
import android.support.media.tv.TvContractCompat.Programs;
import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Base class for derived classes that want to have common fields for programs defined in
* {@link TvContractCompat}.
@@ -46,6 +49,22 @@
private static final int IS_SEARCHABLE = 1;
/** @hide */
+ @IntDef({
+ REVIEW_RATING_STYLE_UNKNOWN,
+ ProgramColumns.REVIEW_RATING_STYLE_STARS,
+ ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
+ ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ @interface ReviewRatingStyle {}
+
+ /**
+ * The unknown review rating style.
+ */
+ private static final int REVIEW_RATING_STYLE_UNKNOWN = -1;
+
+ /** @hide */
@RestrictTo(LIBRARY_GROUP)
protected ContentValues mValues;
@@ -254,7 +273,7 @@
*/
public @ReviewRatingStyle int getReviewRatingStyle() {
Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? REVIEW_RATING_STYLE_UNKNOWN : i;
}
/**
diff --git a/tv-provider/src/main/java/android/support/media/tv/Program.java b/tv-provider/src/main/java/android/support/media/tv/Program.java
index 4e3bd7a..233f1ba 100644
--- a/tv-provider/src/main/java/android/support/media/tv/Program.java
+++ b/tv-provider/src/main/java/android/support/media/tv/Program.java
@@ -25,6 +25,7 @@
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
import android.support.media.tv.TvContractCompat.Programs;
+import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
/**
* A convenience class to access {@link TvContractCompat.Programs} entries in the system content
@@ -282,7 +283,7 @@
* @return This Builder object to allow for chaining of calls to builder methods.
* @see Programs#COLUMN_BROADCAST_GENRE
*/
- public Builder setBroadcastGenres(String[] genres) {
+ public Builder setBroadcastGenres(@Genre String[] genres) {
mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));
return this;
}
diff --git a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
index 5a46e79..de4fd04 100644
--- a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
+++ b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
@@ -30,7 +30,6 @@
import android.os.Build;
import android.os.Bundle;
import android.provider.BaseColumns;
-import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
@@ -606,16 +605,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
interface ProgramColumns {
- /** @hide */
- @IntDef({
- REVIEW_RATING_STYLE_STARS,
- REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
- REVIEW_RATING_STYLE_PERCENTAGE,
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- @interface ReviewRatingStyle {}
-
/**
* The review rating style for five star rating.
*
@@ -934,27 +923,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public interface PreviewProgramColumns {
-
- /** @hide */
- @IntDef({
- TYPE_MOVIE,
- TYPE_TV_SERIES,
- TYPE_TV_SEASON,
- TYPE_TV_EPISODE,
- TYPE_CLIP,
- TYPE_EVENT,
- TYPE_CHANNEL,
- TYPE_TRACK,
- TYPE_ALBUM,
- TYPE_ARTIST,
- TYPE_PLAYLIST,
- TYPE_STATION,
- TYPE_GAME
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- public @interface Type {}
-
/**
* The program type for movie.
*
@@ -1046,19 +1014,6 @@
*/
int TYPE_GAME = 12;
- /** @hide */
- @IntDef({
- ASPECT_RATIO_16_9,
- ASPECT_RATIO_3_2,
- ASPECT_RATIO_4_3,
- ASPECT_RATIO_1_1,
- ASPECT_RATIO_2_3,
- ASPECT_RATIO_MOVIE_POSTER,
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- public @interface AspectRatio {}
-
/**
* The aspect ratio for 16:9.
*
@@ -1107,18 +1062,6 @@
*/
int ASPECT_RATIO_MOVIE_POSTER = 5;
- /** @hide */
- @IntDef({
- AVAILABILITY_AVAILABLE,
- AVAILABILITY_FREE_WITH_SUBSCRIPTION,
- AVAILABILITY_PAID_CONTENT,
- AVAILABILITY_PURCHASED,
- AVAILABILITY_FREE,
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- public @interface Availability {}
-
/**
* The availability for "available to this user".
*
@@ -1155,20 +1098,6 @@
*/
int AVAILABILITY_FREE = 4;
- /** @hide */
- @IntDef({
- INTERACTION_TYPE_VIEWS,
- INTERACTION_TYPE_LISTENS,
- INTERACTION_TYPE_FOLLOWERS,
- INTERACTION_TYPE_FANS,
- INTERACTION_TYPE_LIKES,
- INTERACTION_TYPE_THUMBS,
- INTERACTION_TYPE_VIEWERS,
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- public @interface InteractionType {}
-
/**
* The interaction type for "views".
*
@@ -2895,17 +2824,6 @@
/** The MIME type of a single preview TV program. */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watch_next_program";
- /** @hide */
- @IntDef({
- WATCH_NEXT_TYPE_CONTINUE,
- WATCH_NEXT_TYPE_NEXT,
- WATCH_NEXT_TYPE_NEW,
- WATCH_NEXT_TYPE_WATCHLIST,
- })
- @Retention(RetentionPolicy.SOURCE)
- @RestrictTo(LIBRARY_GROUP)
- public @interface WatchNextType {}
-
/**
* The watch next type for CONTINUE. Use this type when the user has already watched more
* than 1 minute of this content.
diff --git a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
index f466584..c192745 100644
--- a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
@@ -22,12 +22,15 @@
import android.database.Cursor;
import android.media.tv.TvContentRating; // For javadoc gen of super class
import android.os.Build;
+import android.support.annotation.IntDef;
import android.support.annotation.RestrictTo;
import android.support.media.tv.TvContractCompat.PreviewPrograms; // For javadoc gen of super class
import android.support.media.tv.TvContractCompat.Programs; // For javadoc gen of super class
import android.support.media.tv.TvContractCompat.Programs.Genres; // For javadoc gen of super class
import android.support.media.tv.TvContractCompat.WatchNextPrograms;
-import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* A convenience class to access {@link WatchNextPrograms} entries in the system content
@@ -87,16 +90,34 @@
private static final long INVALID_LONG_VALUE = -1;
private static final int INVALID_INT_VALUE = -1;
+ /** @hide */
+ @IntDef({
+ WATCH_NEXT_TYPE_UNKNOWN,
+ WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE,
+ WatchNextPrograms.WATCH_NEXT_TYPE_NEXT,
+ WatchNextPrograms.WATCH_NEXT_TYPE_NEW,
+ WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface WatchNextType {}
+
+ /**
+ * The unknown watch next type. Use this type when the actual type is not known.
+ */
+ public static final int WATCH_NEXT_TYPE_UNKNOWN = -1;
+
private WatchNextProgram(Builder builder) {
super(builder);
}
/**
- * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program.
+ * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program,
+ * or {@link #WATCH_NEXT_TYPE_UNKNOWN} if it's unknown.
*/
public @WatchNextType int getWatchNextType() {
Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
- return i == null ? INVALID_INT_VALUE : i;
+ return i == null ? WATCH_NEXT_TYPE_UNKNOWN : i;
}
/**
diff --git a/v13/java/android/support/v13/app/package.html b/v13/java/android/support/v13/app/package.html
deleted file mode 100755
index 3557ecb..0000000
--- a/v13/java/android/support/v13/app/package.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<body>
-
-Support classes to access some of the android.app package features introduced after API level 13 in a backwards compatible fashion.
-
-</body>
diff --git a/v14/preference/res/layout-v17/preference_category_material.xml b/v14/preference/res/layout-v17/preference_category_material.xml
index db3abfe..804da6a 100644
--- a/v14/preference/res/layout-v17/preference_category_material.xml
+++ b/v14/preference/res/layout-v17/preference_category_material.xml
@@ -15,13 +15,49 @@
~ limitations under the License
-->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/title"
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dip"
- android:textAppearance="@style/Preference_TextAppearanceMaterialBody2"
- android:textColor="@color/preference_fallback_accent_color"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:paddingTop="16dip" />
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+ <android.support.v7.internal.widget.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxHeight="18dp"
+ app:maxWidth="18dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="56dp">
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:textAlignment="viewStart"
+ android:textColor="@color/preference_fallback_accent_color"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/layout-v21/preference_category_material.xml b/v14/preference/res/layout-v21/preference_category_material.xml
index dad9a5c..1331268 100644
--- a/v14/preference/res/layout-v21/preference_category_material.xml
+++ b/v14/preference/res/layout-v21/preference_category_material.xml
@@ -15,13 +15,52 @@
~ limitations under the License
-->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/title"
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dip"
- android:textAppearance="@android:style/TextAppearance.Material.Body2"
- android:textColor="?android:attr/colorAccent"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:paddingTop="16dip" />
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+ <android.support.v7.internal.widget.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxHeight="18dp"
+ app:maxWidth="18dp"
+ android:tint="?android:attr/textColorPrimary"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="56dp">
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:textAlignment="viewStart"
+ android:textAppearance="@android:style/TextAppearance.Material.Body2"
+ android:textColor="?android:attr/colorAccent"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/layout-v21/preference_dropdown_material.xml b/v14/preference/res/layout-v21/preference_dropdown_material.xml
index a92095e..f886d88 100644
--- a/v14/preference/res/layout-v21/preference_dropdown_material.xml
+++ b/v14/preference/res/layout-v21/preference_dropdown_material.xml
@@ -15,74 +15,18 @@
~ limitations under the License
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:gravity="center_vertical"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:background="?android:attr/selectableItemBackground"
- android:clipToPadding="false"
- android:focusable="true" >
+ android:layout_height="wrap_content">
<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/preference_no_icon_padding_start"
android:visibility="invisible" />
- <LinearLayout
- android:id="@+id/icon_frame"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="-4dp"
- android:minWidth="60dp"
- android:gravity="start|center_vertical"
- android:orientation="horizontal"
- android:paddingRight="12dp"
- android:paddingTop="4dp"
- android:paddingBottom="4dp">
- <android.support.v7.internal.widget.PreferenceImageView
- android:id="@android:id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:maxWidth="48dp"
- app:maxHeight="48dp" />
- </LinearLayout>
+ <include layout="@layout/preference_material"/>
- <RelativeLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="16dp"
- android:paddingBottom="16dp">
-
- <TextView android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:singleLine="true"
- android:textAppearance="@style/Preference_TextAppearanceMaterialSubhead"
- android:ellipsize="marquee" />
-
- <TextView android:id="@android:id/summary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_alignStart="@android:id/title"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="10" />
-
- </RelativeLayout>
-
- <!-- Preference should place its actual preference widget here. -->
- <LinearLayout android:id="@android:id/widget_frame"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="end|center_vertical"
- android:paddingLeft="16dp"
- android:orientation="vertical" />
-
-</LinearLayout>
+</FrameLayout>
diff --git a/v14/preference/res/layout/preference_category_material.xml b/v14/preference/res/layout/preference_category_material.xml
index e366e7a..8eb2137 100644
--- a/v14/preference/res/layout/preference_category_material.xml
+++ b/v14/preference/res/layout/preference_category_material.xml
@@ -15,13 +15,49 @@
~ limitations under the License
-->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/title"
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dip"
- android:textAppearance="@style/Preference_TextAppearanceMaterialBody2"
- android:textColor="@color/preference_fallback_accent_color"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- android:paddingRight="?android:attr/listPreferredItemPaddingRight"
- android:paddingTop="16dip" />
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+ <android.support.v7.internal.widget.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxHeight="18dp"
+ app:maxWidth="18dp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="56dp">
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:textAlignment="viewStart"
+ android:textColor="@color/preference_fallback_accent_color"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/values-v21/styles.xml b/v14/preference/res/values-v21/styles.xml
new file mode 100644
index 0000000..9a85987
--- /dev/null
+++ b/v14/preference/res/values-v21/styles.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <dimen name="preference_no_icon_padding_start">72dp</dimen>
+</resources>
+
diff --git a/v14/preference/res/values/styles.xml b/v14/preference/res/values/styles.xml
index 26b1544..edd5285 100644
--- a/v14/preference/res/values/styles.xml
+++ b/v14/preference/res/values/styles.xml
@@ -24,6 +24,10 @@
<style name="Preference.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">false</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.Information.Material">
@@ -34,10 +38,16 @@
<style name="Preference.Category.Material">
<item name="android:layout">@layout/preference_category_material</item>
+ <item name="allowDividerAbove">true</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.CheckBoxPreference.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.SwitchPreferenceCompat.Material">
@@ -46,6 +56,10 @@
<style name="Preference.SwitchPreference.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">false</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.SeekBarPreference.Material">
@@ -56,18 +70,31 @@
<style name="Preference.PreferenceScreen.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.DialogPreference.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.DialogPreference.EditTextPreference.Material">
<item name="android:layout">@layout/preference_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="singleLineTitle">false</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference.DropDown.Material">
<item name="android:layout">@layout/preference_dropdown_material</item>
+ <item name="allowDividerAbove">false</item>
+ <item name="allowDividerBelow">true</item>
+ <item name="iconSpaceReserved">true</item>
</style>
<style name="Preference_TextAppearanceMaterialBody2">
@@ -86,6 +113,7 @@
<style name="PreferenceFragment.Material">
<item name="android:divider">@drawable/preference_list_divider_material</item>
+ <item name="allowDividerAfterLastItem">false</item>
</style>
<style name="PreferenceFragmentList.Material">
diff --git a/v14/preference/res/values/themes.xml b/v14/preference/res/values/themes.xml
index a69126f..919873e 100644
--- a/v14/preference/res/values/themes.xml
+++ b/v14/preference/res/values/themes.xml
@@ -36,5 +36,6 @@
<item name="editTextPreferenceStyle">@style/Preference.DialogPreference.EditTextPreference.Material</item>
<item name="dropdownPreferenceStyle">@style/Preference.DropDown.Material</item>
<item name="preferenceFragmentListStyle">@style/PreferenceFragmentList.Material</item>
+ <item name="android:scrollbars">vertical</item>
</style>
</resources>
diff --git a/v17/Android.mk b/v17/Android.mk
deleted file mode 100644
index 14ff0aa..0000000
--- a/v17/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH:= $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/v17/leanback/api/current.txt b/v17/leanback/api/current.txt
deleted file mode 100644
index 4ee4d94..0000000
--- a/v17/leanback/api/current.txt
+++ /dev/null
@@ -1,3129 +0,0 @@
-package android.support.v17.leanback.app {
-
- public final class BackgroundManager {
- method public void attach(android.view.Window);
- method public void attachToView(android.view.View);
- method public void clearDrawable();
- method public final int getColor();
- method public deprecated android.graphics.drawable.Drawable getDefaultDimLayer();
- method public deprecated android.graphics.drawable.Drawable getDimLayer();
- method public android.graphics.drawable.Drawable getDrawable();
- method public static android.support.v17.leanback.app.BackgroundManager getInstance(android.app.Activity);
- method public boolean isAttached();
- method public boolean isAutoReleaseOnStop();
- method public void release();
- method public void setAutoReleaseOnStop(boolean);
- method public void setBitmap(android.graphics.Bitmap);
- method public void setColor(int);
- method public deprecated void setDimLayer(android.graphics.drawable.Drawable);
- method public void setDrawable(android.graphics.drawable.Drawable);
- method public void setThemeDrawableResourceId(int);
- }
-
- public class BaseFragment extends android.support.v17.leanback.app.BrandedFragment {
- method protected java.lang.Object createEntranceTransition();
- method public final android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
- method protected void onEntranceTransitionEnd();
- method protected void onEntranceTransitionPrepare();
- method protected void onEntranceTransitionStart();
- method public void prepareEntranceTransition();
- method protected void runEntranceTransition(java.lang.Object);
- method public void startEntranceTransition();
- }
-
- abstract class BaseRowFragment extends android.app.Fragment {
- method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
- method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
- method public int getSelectedPosition();
- method public final android.support.v17.leanback.widget.VerticalGridView getVerticalGridView();
- method public void onTransitionEnd();
- method public boolean onTransitionPrepare();
- method public void onTransitionStart();
- method public final void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setAlignment(int);
- method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- }
-
- abstract class BaseRowSupportFragment extends android.support.v4.app.Fragment {
- method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
- method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
- method public int getSelectedPosition();
- method public final android.support.v17.leanback.widget.VerticalGridView getVerticalGridView();
- method public void onTransitionEnd();
- method public boolean onTransitionPrepare();
- method public void onTransitionStart();
- method public final void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setAlignment(int);
- method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- }
-
- public class BaseSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
- method protected java.lang.Object createEntranceTransition();
- method public final android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
- method protected void onEntranceTransitionEnd();
- method protected void onEntranceTransitionPrepare();
- method protected void onEntranceTransitionStart();
- method public void prepareEntranceTransition();
- method protected void runEntranceTransition(java.lang.Object);
- method public void startEntranceTransition();
- }
-
- public class BrandedFragment extends android.app.Fragment {
- ctor public BrandedFragment();
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public int getSearchAffordanceColor();
- method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
- method public java.lang.CharSequence getTitle();
- method public android.view.View getTitleView();
- method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
- method public void installTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public final boolean isShowingTitle();
- method public android.view.View onInflateTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnSearchClickedListener(android.view.View.OnClickListener);
- method public void setSearchAffordanceColor(int);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setTitle(java.lang.CharSequence);
- method public void setTitleView(android.view.View);
- method public void showTitle(boolean);
- method public void showTitle(int);
- }
-
- public class BrandedSupportFragment extends android.support.v4.app.Fragment {
- ctor public BrandedSupportFragment();
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public int getSearchAffordanceColor();
- method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
- method public java.lang.CharSequence getTitle();
- method public android.view.View getTitleView();
- method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
- method public void installTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public final boolean isShowingTitle();
- method public android.view.View onInflateTitleView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnSearchClickedListener(android.view.View.OnClickListener);
- method public void setSearchAffordanceColor(int);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setTitle(java.lang.CharSequence);
- method public void setTitleView(android.view.View);
- method public void showTitle(boolean);
- method public void showTitle(int);
- }
-
- public class BrowseFragment extends android.support.v17.leanback.app.BaseFragment {
- ctor public BrowseFragment();
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, int);
- method public void enableMainFragmentScaling(boolean);
- method public deprecated void enableRowScaling(boolean);
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public int getBrandColor();
- method public android.support.v17.leanback.app.HeadersFragment getHeadersFragment();
- method public int getHeadersState();
- method public android.app.Fragment getMainFragment();
- method public final android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterRegistry getMainFragmentRegistry();
- method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
- method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
- method public int getSelectedPosition();
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getSelectedRowViewHolder();
- method public final boolean isHeadersTransitionOnBackEnabled();
- method public boolean isInHeadersTransition();
- method public boolean isShowingHeaders();
- method public android.support.v17.leanback.app.HeadersFragment onCreateHeadersFragment();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setBrandColor(int);
- method public void setBrowseTransitionListener(android.support.v17.leanback.app.BrowseFragment.BrowseTransitionListener);
- method public void setHeaderPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public void setHeadersState(int);
- method public final void setHeadersTransitionOnBackEnabled(boolean);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- method public void startHeadersTransition(boolean);
- field public static final int HEADERS_DISABLED = 3; // 0x3
- field public static final int HEADERS_ENABLED = 1; // 0x1
- field public static final int HEADERS_HIDDEN = 2; // 0x2
- }
-
- public static class BrowseFragment.BrowseTransitionListener {
- ctor public BrowseFragment.BrowseTransitionListener();
- method public void onHeadersTransitionStart(boolean);
- method public void onHeadersTransitionStop(boolean);
- }
-
- public static abstract class BrowseFragment.FragmentFactory<T extends android.app.Fragment> {
- ctor public BrowseFragment.FragmentFactory();
- method public abstract T createFragment(java.lang.Object);
- }
-
- public static abstract interface BrowseFragment.FragmentHost {
- method public abstract void notifyDataReady(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
- method public abstract void notifyViewCreated(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
- method public abstract void showTitleView(boolean);
- }
-
- public static class BrowseFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseFragment.FragmentFactory {
- ctor public BrowseFragment.ListRowFragmentFactory();
- method public android.support.v17.leanback.app.RowsFragment createFragment(java.lang.Object);
- }
-
- public static class BrowseFragment.MainFragmentAdapter<T extends android.app.Fragment> {
- ctor public BrowseFragment.MainFragmentAdapter(T);
- method public final T getFragment();
- method public final android.support.v17.leanback.app.BrowseFragment.FragmentHost getFragmentHost();
- method public boolean isScalingEnabled();
- method public boolean isScrolling();
- method public void onTransitionEnd();
- method public boolean onTransitionPrepare();
- method public void onTransitionStart();
- method public void setAlignment(int);
- method public void setEntranceTransitionState(boolean);
- method public void setExpand(boolean);
- method public void setScalingEnabled(boolean);
- }
-
- public static abstract interface BrowseFragment.MainFragmentAdapterProvider {
- method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter getMainFragmentAdapter();
- }
-
- public static final class BrowseFragment.MainFragmentAdapterRegistry {
- ctor public BrowseFragment.MainFragmentAdapterRegistry();
- method public android.app.Fragment createFragment(java.lang.Object);
- method public void registerFragment(java.lang.Class, android.support.v17.leanback.app.BrowseFragment.FragmentFactory);
- }
-
- public static class BrowseFragment.MainFragmentRowsAdapter<T extends android.app.Fragment> {
- ctor public BrowseFragment.MainFragmentRowsAdapter(T);
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
- method public final T getFragment();
- method public int getSelectedPosition();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- method public void setSelectedPosition(int, boolean);
- }
-
- public static abstract interface BrowseFragment.MainFragmentRowsAdapterProvider {
- method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- }
-
- public class BrowseSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
- ctor public BrowseSupportFragment();
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, int);
- method public void enableMainFragmentScaling(boolean);
- method public deprecated void enableRowScaling(boolean);
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public int getBrandColor();
- method public int getHeadersState();
- method public android.support.v17.leanback.app.HeadersSupportFragment getHeadersSupportFragment();
- method public android.support.v4.app.Fragment getMainFragment();
- method public final android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapterRegistry getMainFragmentRegistry();
- method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
- method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
- method public int getSelectedPosition();
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getSelectedRowViewHolder();
- method public final boolean isHeadersTransitionOnBackEnabled();
- method public boolean isInHeadersTransition();
- method public boolean isShowingHeaders();
- method public android.support.v17.leanback.app.HeadersSupportFragment onCreateHeadersSupportFragment();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setBrandColor(int);
- method public void setBrowseTransitionListener(android.support.v17.leanback.app.BrowseSupportFragment.BrowseTransitionListener);
- method public void setHeaderPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public void setHeadersState(int);
- method public final void setHeadersTransitionOnBackEnabled(boolean);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- method public void startHeadersTransition(boolean);
- field public static final int HEADERS_DISABLED = 3; // 0x3
- field public static final int HEADERS_ENABLED = 1; // 0x1
- field public static final int HEADERS_HIDDEN = 2; // 0x2
- }
-
- public static class BrowseSupportFragment.BrowseTransitionListener {
- ctor public BrowseSupportFragment.BrowseTransitionListener();
- method public void onHeadersTransitionStart(boolean);
- method public void onHeadersTransitionStop(boolean);
- }
-
- public static abstract class BrowseSupportFragment.FragmentFactory<T extends android.support.v4.app.Fragment> {
- ctor public BrowseSupportFragment.FragmentFactory();
- method public abstract T createFragment(java.lang.Object);
- }
-
- public static abstract interface BrowseSupportFragment.FragmentHost {
- method public abstract void notifyDataReady(android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter);
- method public abstract void notifyViewCreated(android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter);
- method public abstract void showTitleView(boolean);
- }
-
- public static class BrowseSupportFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseSupportFragment.FragmentFactory {
- ctor public BrowseSupportFragment.ListRowFragmentFactory();
- method public android.support.v17.leanback.app.RowsSupportFragment createFragment(java.lang.Object);
- }
-
- public static class BrowseSupportFragment.MainFragmentAdapter<T extends android.support.v4.app.Fragment> {
- ctor public BrowseSupportFragment.MainFragmentAdapter(T);
- method public final T getFragment();
- method public final android.support.v17.leanback.app.BrowseSupportFragment.FragmentHost getFragmentHost();
- method public boolean isScalingEnabled();
- method public boolean isScrolling();
- method public void onTransitionEnd();
- method public boolean onTransitionPrepare();
- method public void onTransitionStart();
- method public void setAlignment(int);
- method public void setEntranceTransitionState(boolean);
- method public void setExpand(boolean);
- method public void setScalingEnabled(boolean);
- }
-
- public static abstract interface BrowseSupportFragment.MainFragmentAdapterProvider {
- method public abstract android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter();
- }
-
- public static final class BrowseSupportFragment.MainFragmentAdapterRegistry {
- ctor public BrowseSupportFragment.MainFragmentAdapterRegistry();
- method public android.support.v4.app.Fragment createFragment(java.lang.Object);
- method public void registerFragment(java.lang.Class, android.support.v17.leanback.app.BrowseSupportFragment.FragmentFactory);
- }
-
- public static class BrowseSupportFragment.MainFragmentRowsAdapter<T extends android.support.v4.app.Fragment> {
- ctor public BrowseSupportFragment.MainFragmentRowsAdapter(T);
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
- method public final T getFragment();
- method public int getSelectedPosition();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- method public void setSelectedPosition(int, boolean);
- }
-
- public static abstract interface BrowseSupportFragment.MainFragmentRowsAdapterProvider {
- method public abstract android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- }
-
- public class DetailsFragment extends android.support.v17.leanback.app.BaseFragment {
- ctor public DetailsFragment();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.DetailsParallax getParallax();
- method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
- method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method protected void onSetDetailsOverviewRowStatus(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int);
- method protected void onSetRowStatus(android.support.v17.leanback.widget.RowPresenter, android.support.v17.leanback.widget.RowPresenter.ViewHolder, int, int, int);
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method protected void setupDetailsOverviewRowPresenter(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
- method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
- }
-
- public class DetailsFragmentBackgroundController {
- ctor public DetailsFragmentBackgroundController(android.support.v17.leanback.app.DetailsFragment);
- method public boolean canNavigateToVideoFragment();
- method public void enableParallax();
- method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
- method public final android.app.Fragment findOrCreateVideoFragment();
- method public final android.graphics.drawable.Drawable getBottomDrawable();
- method public final android.graphics.Bitmap getCoverBitmap();
- method public final android.graphics.drawable.Drawable getCoverDrawable();
- method public final int getParallaxDrawableMaxOffset();
- method public final android.support.v17.leanback.media.PlaybackGlue getPlaybackGlue();
- method public final int getSolidColor();
- method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
- method public android.app.Fragment onCreateVideoFragment();
- method public final void setCoverBitmap(android.graphics.Bitmap);
- method public final void setParallaxDrawableMaxOffset(int);
- method public final void setSolidColor(int);
- method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
- method public final void switchToRows();
- method public final void switchToVideo();
- }
-
- public class DetailsSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
- ctor public DetailsSupportFragment();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.DetailsParallax getParallax();
- method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
- method protected deprecated android.view.View inflateTitle(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method protected void onSetDetailsOverviewRowStatus(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int);
- method protected void onSetRowStatus(android.support.v17.leanback.widget.RowPresenter, android.support.v17.leanback.widget.RowPresenter.ViewHolder, int, int, int);
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method protected void setupDetailsOverviewRowPresenter(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
- method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
- }
-
- public class DetailsSupportFragmentBackgroundController {
- ctor public DetailsSupportFragmentBackgroundController(android.support.v17.leanback.app.DetailsSupportFragment);
- method public boolean canNavigateToVideoSupportFragment();
- method public void enableParallax();
- method public void enableParallax(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable, android.support.v17.leanback.widget.ParallaxTarget.PropertyValuesHolderTarget);
- method public final android.support.v4.app.Fragment findOrCreateVideoSupportFragment();
- method public final android.graphics.drawable.Drawable getBottomDrawable();
- method public final android.graphics.Bitmap getCoverBitmap();
- method public final android.graphics.drawable.Drawable getCoverDrawable();
- method public final int getParallaxDrawableMaxOffset();
- method public final android.support.v17.leanback.media.PlaybackGlue getPlaybackGlue();
- method public final int getSolidColor();
- method public android.support.v17.leanback.media.PlaybackGlueHost onCreateGlueHost();
- method public android.support.v4.app.Fragment onCreateVideoSupportFragment();
- method public final void setCoverBitmap(android.graphics.Bitmap);
- method public final void setParallaxDrawableMaxOffset(int);
- method public final void setSolidColor(int);
- method public void setupVideoPlayback(android.support.v17.leanback.media.PlaybackGlue);
- method public final void switchToRows();
- method public final void switchToVideo();
- }
-
- public class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
- ctor public ErrorFragment();
- method public android.graphics.drawable.Drawable getBackgroundDrawable();
- method public android.view.View.OnClickListener getButtonClickListener();
- method public java.lang.String getButtonText();
- method public android.graphics.drawable.Drawable getImageDrawable();
- method public java.lang.CharSequence getMessage();
- method public boolean isBackgroundTranslucent();
- method public void setBackgroundDrawable(android.graphics.drawable.Drawable);
- method public void setButtonClickListener(android.view.View.OnClickListener);
- method public void setButtonText(java.lang.String);
- method public void setDefaultBackground(boolean);
- method public void setImageDrawable(android.graphics.drawable.Drawable);
- method public void setMessage(java.lang.CharSequence);
- }
-
- public class ErrorSupportFragment extends android.support.v17.leanback.app.BrandedSupportFragment {
- ctor public ErrorSupportFragment();
- method public android.graphics.drawable.Drawable getBackgroundDrawable();
- method public android.view.View.OnClickListener getButtonClickListener();
- method public java.lang.String getButtonText();
- method public android.graphics.drawable.Drawable getImageDrawable();
- method public java.lang.CharSequence getMessage();
- method public boolean isBackgroundTranslucent();
- method public void setBackgroundDrawable(android.graphics.drawable.Drawable);
- method public void setButtonClickListener(android.view.View.OnClickListener);
- method public void setButtonText(java.lang.String);
- method public void setDefaultBackground(boolean);
- method public void setImageDrawable(android.graphics.drawable.Drawable);
- method public void setMessage(java.lang.CharSequence);
- }
-
- public class GuidedStepFragment extends android.app.Fragment {
- ctor public GuidedStepFragment();
- method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment);
- method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment, int);
- method public static int addAsRoot(android.app.Activity, android.support.v17.leanback.app.GuidedStepFragment, int);
- method public void collapseAction(boolean);
- method public void collapseSubActions();
- method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
- method public void expandSubActions(android.support.v17.leanback.widget.GuidedAction);
- method public android.support.v17.leanback.widget.GuidedAction findActionById(long);
- method public int findActionPositionById(long);
- method public android.support.v17.leanback.widget.GuidedAction findButtonActionById(long);
- method public int findButtonActionPositionById(long);
- method public void finishGuidedStepFragments();
- method public android.view.View getActionItemView(int);
- method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getActions();
- method public android.view.View getButtonActionItemView(int);
- method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getButtonActions();
- method public static android.support.v17.leanback.app.GuidedStepFragment getCurrentGuidedStepFragment(android.app.FragmentManager);
- method public android.support.v17.leanback.widget.GuidanceStylist getGuidanceStylist();
- method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedActionsStylist();
- method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedButtonActionsStylist();
- method public int getSelectedActionPosition();
- method public int getSelectedButtonActionPosition();
- method public int getUiStyle();
- method public boolean isExpanded();
- method public boolean isFocusOutEndAllowed();
- method public boolean isFocusOutStartAllowed();
- method public boolean isSubActionsExpanded();
- method public void notifyActionChanged(int);
- method public void notifyButtonActionChanged(int);
- method protected void onAddSharedElementTransition(android.app.FragmentTransaction, android.support.v17.leanback.app.GuidedStepFragment);
- method public void onCreateActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateActionsStylist();
- method public android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public void onCreateButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateButtonActionsStylist();
- method public android.support.v17.leanback.widget.GuidanceStylist.Guidance onCreateGuidance(android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidanceStylist onCreateGuidanceStylist();
- method public void onGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
- method public void onGuidedActionEditCanceled(android.support.v17.leanback.widget.GuidedAction);
- method public deprecated void onGuidedActionEdited(android.support.v17.leanback.widget.GuidedAction);
- method public long onGuidedActionEditedAndProceed(android.support.v17.leanback.widget.GuidedAction);
- method public void onGuidedActionFocused(android.support.v17.leanback.widget.GuidedAction);
- method protected void onProvideFragmentTransitions();
- method public int onProvideTheme();
- method public boolean onSubGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
- method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
- method public void popBackStackToGuidedStepFragment(java.lang.Class, int);
- method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public void setSelectedActionPosition(int);
- method public void setSelectedButtonActionPosition(int);
- method public void setUiStyle(int);
- field public static final java.lang.String EXTRA_UI_STYLE = "uiStyle";
- field public static final int UI_STYLE_ACTIVITY_ROOT = 2; // 0x2
- field public static final deprecated int UI_STYLE_DEFAULT = 0; // 0x0
- field public static final int UI_STYLE_ENTRANCE = 1; // 0x1
- field public static final int UI_STYLE_REPLACE = 0; // 0x0
- }
-
- public class GuidedStepSupportFragment extends android.support.v4.app.Fragment {
- ctor public GuidedStepSupportFragment();
- method public static int add(android.support.v4.app.FragmentManager, android.support.v17.leanback.app.GuidedStepSupportFragment);
- method public static int add(android.support.v4.app.FragmentManager, android.support.v17.leanback.app.GuidedStepSupportFragment, int);
- method public static int addAsRoot(android.support.v4.app.FragmentActivity, android.support.v17.leanback.app.GuidedStepSupportFragment, int);
- method public void collapseAction(boolean);
- method public void collapseSubActions();
- method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
- method public void expandSubActions(android.support.v17.leanback.widget.GuidedAction);
- method public android.support.v17.leanback.widget.GuidedAction findActionById(long);
- method public int findActionPositionById(long);
- method public android.support.v17.leanback.widget.GuidedAction findButtonActionById(long);
- method public int findButtonActionPositionById(long);
- method public void finishGuidedStepSupportFragments();
- method public android.view.View getActionItemView(int);
- method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getActions();
- method public android.view.View getButtonActionItemView(int);
- method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getButtonActions();
- method public static android.support.v17.leanback.app.GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(android.support.v4.app.FragmentManager);
- method public android.support.v17.leanback.widget.GuidanceStylist getGuidanceStylist();
- method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedActionsStylist();
- method public android.support.v17.leanback.widget.GuidedActionsStylist getGuidedButtonActionsStylist();
- method public int getSelectedActionPosition();
- method public int getSelectedButtonActionPosition();
- method public int getUiStyle();
- method public boolean isExpanded();
- method public boolean isFocusOutEndAllowed();
- method public boolean isFocusOutStartAllowed();
- method public boolean isSubActionsExpanded();
- method public void notifyActionChanged(int);
- method public void notifyButtonActionChanged(int);
- method protected void onAddSharedElementTransition(android.support.v4.app.FragmentTransaction, android.support.v17.leanback.app.GuidedStepSupportFragment);
- method public void onCreateActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateActionsStylist();
- method public android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle);
- method public void onCreateButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>, android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidedActionsStylist onCreateButtonActionsStylist();
- method public android.support.v17.leanback.widget.GuidanceStylist.Guidance onCreateGuidance(android.os.Bundle);
- method public android.support.v17.leanback.widget.GuidanceStylist onCreateGuidanceStylist();
- method public void onGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
- method public void onGuidedActionEditCanceled(android.support.v17.leanback.widget.GuidedAction);
- method public deprecated void onGuidedActionEdited(android.support.v17.leanback.widget.GuidedAction);
- method public long onGuidedActionEditedAndProceed(android.support.v17.leanback.widget.GuidedAction);
- method public void onGuidedActionFocused(android.support.v17.leanback.widget.GuidedAction);
- method protected void onProvideFragmentTransitions();
- method public int onProvideTheme();
- method public boolean onSubGuidedActionClicked(android.support.v17.leanback.widget.GuidedAction);
- method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
- method public void popBackStackToGuidedStepSupportFragment(java.lang.Class, int);
- method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public void setSelectedActionPosition(int);
- method public void setSelectedButtonActionPosition(int);
- method public void setUiStyle(int);
- field public static final java.lang.String EXTRA_UI_STYLE = "uiStyle";
- field public static final int UI_STYLE_ACTIVITY_ROOT = 2; // 0x2
- field public static final deprecated int UI_STYLE_DEFAULT = 0; // 0x0
- field public static final int UI_STYLE_ENTRANCE = 1; // 0x1
- field public static final int UI_STYLE_REPLACE = 0; // 0x0
- }
-
- public class HeadersFragment extends android.support.v17.leanback.app.BaseRowFragment {
- ctor public HeadersFragment();
- method public boolean isScrolling();
- method public void setOnHeaderClickedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderClickedListener);
- method public void setOnHeaderViewSelectedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderViewSelectedListener);
- }
-
- public static abstract interface HeadersFragment.OnHeaderClickedListener {
- method public abstract void onHeaderClicked(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
- }
-
- public static abstract interface HeadersFragment.OnHeaderViewSelectedListener {
- method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
- }
-
- public class HeadersSupportFragment extends android.support.v17.leanback.app.BaseRowSupportFragment {
- ctor public HeadersSupportFragment();
- method public boolean isScrolling();
- method public void setOnHeaderClickedListener(android.support.v17.leanback.app.HeadersSupportFragment.OnHeaderClickedListener);
- method public void setOnHeaderViewSelectedListener(android.support.v17.leanback.app.HeadersSupportFragment.OnHeaderViewSelectedListener);
- }
-
- public static abstract interface HeadersSupportFragment.OnHeaderClickedListener {
- method public abstract void onHeaderClicked(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
- }
-
- public static abstract interface HeadersSupportFragment.OnHeaderViewSelectedListener {
- method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
- }
-
- public abstract class OnboardingFragment extends android.app.Fragment {
- ctor public OnboardingFragment();
- method public final int getArrowBackgroundColor();
- method public final int getArrowColor();
- method protected final int getCurrentPageIndex();
- method public final int getDescriptionViewTextColor();
- method public final int getDotBackgroundColor();
- method public final int getIconResourceId();
- method public final int getLogoResourceId();
- method protected abstract int getPageCount();
- method protected abstract java.lang.CharSequence getPageDescription(int);
- method protected abstract java.lang.CharSequence getPageTitle(int);
- method public final java.lang.CharSequence getStartButtonText();
- method public final int getTitleViewTextColor();
- method protected final boolean isLogoAnimationFinished();
- method protected void moveToNextPage();
- method protected void moveToPreviousPage();
- method protected abstract android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected abstract android.view.View onCreateContentView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected android.animation.Animator onCreateDescriptionAnimator();
- method protected android.animation.Animator onCreateEnterAnimation();
- method protected abstract android.view.View onCreateForegroundView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected android.animation.Animator onCreateLogoAnimation();
- method protected android.animation.Animator onCreateTitleAnimator();
- method protected void onFinishFragment();
- method protected void onLogoAnimationFinished();
- method protected void onPageChanged(int, int);
- method public int onProvideTheme();
- method public void setArrowBackgroundColor(int);
- method public void setArrowColor(int);
- method public void setDescriptionViewTextColor(int);
- method public void setDotBackgroundColor(int);
- method public final void setIconResouceId(int);
- method public final void setLogoResourceId(int);
- method public void setStartButtonText(java.lang.CharSequence);
- method public void setTitleViewTextColor(int);
- method protected final void startEnterAnimation(boolean);
- }
-
- public abstract class OnboardingSupportFragment extends android.support.v4.app.Fragment {
- ctor public OnboardingSupportFragment();
- method public final int getArrowBackgroundColor();
- method public final int getArrowColor();
- method protected final int getCurrentPageIndex();
- method public final int getDescriptionViewTextColor();
- method public final int getDotBackgroundColor();
- method public final int getIconResourceId();
- method public final int getLogoResourceId();
- method protected abstract int getPageCount();
- method protected abstract java.lang.CharSequence getPageDescription(int);
- method protected abstract java.lang.CharSequence getPageTitle(int);
- method public final java.lang.CharSequence getStartButtonText();
- method public final int getTitleViewTextColor();
- method protected final boolean isLogoAnimationFinished();
- method protected void moveToNextPage();
- method protected void moveToPreviousPage();
- method protected abstract android.view.View onCreateBackgroundView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected abstract android.view.View onCreateContentView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected android.animation.Animator onCreateDescriptionAnimator();
- method protected android.animation.Animator onCreateEnterAnimation();
- method protected abstract android.view.View onCreateForegroundView(android.view.LayoutInflater, android.view.ViewGroup);
- method protected android.animation.Animator onCreateLogoAnimation();
- method protected android.animation.Animator onCreateTitleAnimator();
- method protected void onFinishFragment();
- method protected void onLogoAnimationFinished();
- method protected void onPageChanged(int, int);
- method public int onProvideTheme();
- method public void setArrowBackgroundColor(int);
- method public void setArrowColor(int);
- method public void setDescriptionViewTextColor(int);
- method public void setDotBackgroundColor(int);
- method public final void setIconResouceId(int);
- method public final void setLogoResourceId(int);
- method public void setStartButtonText(java.lang.CharSequence);
- method public void setTitleViewTextColor(int);
- method protected final void startEnterAnimation(boolean);
- }
-
- public class PlaybackFragment extends android.app.Fragment {
- ctor public PlaybackFragment();
- method public deprecated void fadeOut();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public int getBackgroundType();
- method public android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
- method public void hideControlsOverlay(boolean);
- method public boolean isControlsOverlayAutoHideEnabled();
- method public boolean isControlsOverlayVisible();
- method public deprecated boolean isFadingEnabled();
- method public void notifyPlaybackRowChanged();
- method protected void onBufferingStateChanged(boolean);
- method protected void onError(int, java.lang.CharSequence);
- method protected void onVideoSizeChanged(int, int);
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setBackgroundType(int);
- method public void setControlsOverlayAutoHideEnabled(boolean);
- method public deprecated void setFadingEnabled(boolean);
- method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
- method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
- method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
- method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method public void showControlsOverlay(boolean);
- method public void tickle();
- field public static final int BG_DARK = 1; // 0x1
- field public static final int BG_LIGHT = 2; // 0x2
- field public static final int BG_NONE = 0; // 0x0
- }
-
- public class PlaybackFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
- ctor public PlaybackFragmentGlueHost(android.support.v17.leanback.app.PlaybackFragment);
- method public void fadeOut();
- method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- }
-
- public class PlaybackSupportFragment extends android.support.v4.app.Fragment {
- ctor public PlaybackSupportFragment();
- method public deprecated void fadeOut();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public int getBackgroundType();
- method public android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
- method public void hideControlsOverlay(boolean);
- method public boolean isControlsOverlayAutoHideEnabled();
- method public boolean isControlsOverlayVisible();
- method public deprecated boolean isFadingEnabled();
- method public void notifyPlaybackRowChanged();
- method protected void onBufferingStateChanged(boolean);
- method protected void onError(int, java.lang.CharSequence);
- method protected void onVideoSizeChanged(int, int);
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setBackgroundType(int);
- method public void setControlsOverlayAutoHideEnabled(boolean);
- method public deprecated void setFadingEnabled(boolean);
- method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public final void setOnKeyInterceptListener(android.view.View.OnKeyListener);
- method public void setOnPlaybackItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
- method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
- method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, boolean);
- method public void showControlsOverlay(boolean);
- method public void tickle();
- field public static final int BG_DARK = 1; // 0x1
- field public static final int BG_LIGHT = 2; // 0x2
- field public static final int BG_NONE = 0; // 0x0
- }
-
- public class PlaybackSupportFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
- ctor public PlaybackSupportFragmentGlueHost(android.support.v17.leanback.app.PlaybackSupportFragment);
- method public void fadeOut();
- method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- }
-
- public final class ProgressBarManager {
- ctor public ProgressBarManager();
- method public void disableProgressBar();
- method public void enableProgressBar();
- method public long getInitialDelay();
- method public void hide();
- method public void setInitialDelay(long);
- method public void setProgressBarView(android.view.View);
- method public void setRootView(android.view.ViewGroup);
- method public void show();
- }
-
- public class RowsFragment extends android.support.v17.leanback.app.BaseRowFragment implements android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapterProvider {
- ctor public RowsFragment();
- method public deprecated void enableRowScaling(boolean);
- method protected android.support.v17.leanback.widget.VerticalGridView findGridViewFromRoot(android.view.View);
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
- method public android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter getMainFragmentAdapter();
- method public android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(int);
- method public boolean isScrolling();
- method public void setEntranceTransitionState(boolean);
- method public void setExpand(boolean);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- }
-
- public static class RowsFragment.MainFragmentAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter {
- ctor public RowsFragment.MainFragmentAdapter(android.support.v17.leanback.app.RowsFragment);
- }
-
- public static class RowsFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter {
- ctor public RowsFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsFragment);
- }
-
- public class RowsSupportFragment extends android.support.v17.leanback.app.BaseRowSupportFragment implements android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapterProvider {
- ctor public RowsSupportFragment();
- method public deprecated void enableRowScaling(boolean);
- method protected android.support.v17.leanback.widget.VerticalGridView findGridViewFromRoot(android.view.View);
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
- method public android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter();
- method public android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
- method public android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
- method public android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(int);
- method public boolean isScrolling();
- method public void setEntranceTransitionState(boolean);
- method public void setExpand(boolean);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public void setSelectedPosition(int, boolean, android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- }
-
- public static class RowsSupportFragment.MainFragmentAdapter extends android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentAdapter {
- ctor public RowsSupportFragment.MainFragmentAdapter(android.support.v17.leanback.app.RowsSupportFragment);
- }
-
- public static class RowsSupportFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter {
- ctor public RowsSupportFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsSupportFragment);
- }
-
- public class SearchFragment extends android.app.Fragment {
- ctor public SearchFragment();
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String);
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, java.lang.String);
- method public void displayCompletions(java.util.List<java.lang.String>);
- method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public android.content.Intent getRecognizerIntent();
- method public android.support.v17.leanback.app.RowsFragment getRowsFragment();
- method public java.lang.String getTitle();
- method public static android.support.v17.leanback.app.SearchFragment newInstance(java.lang.String);
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchQuery(java.lang.String, boolean);
- method public void setSearchQuery(android.content.Intent, boolean);
- method public void setSearchResultProvider(android.support.v17.leanback.app.SearchFragment.SearchResultProvider);
- method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
- method public void setTitle(java.lang.String);
- method public void startRecognition();
- }
-
- public static abstract interface SearchFragment.SearchResultProvider {
- method public abstract android.support.v17.leanback.widget.ObjectAdapter getResultsAdapter();
- method public abstract boolean onQueryTextChange(java.lang.String);
- method public abstract boolean onQueryTextSubmit(java.lang.String);
- }
-
- public class SearchSupportFragment extends android.support.v4.app.Fragment {
- ctor public SearchSupportFragment();
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String);
- method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, java.lang.String);
- method public void displayCompletions(java.util.List<java.lang.String>);
- method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public android.content.Intent getRecognizerIntent();
- method public android.support.v17.leanback.app.RowsSupportFragment getRowsSupportFragment();
- method public java.lang.String getTitle();
- method public static android.support.v17.leanback.app.SearchSupportFragment newInstance(java.lang.String);
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchQuery(java.lang.String, boolean);
- method public void setSearchQuery(android.content.Intent, boolean);
- method public void setSearchResultProvider(android.support.v17.leanback.app.SearchSupportFragment.SearchResultProvider);
- method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
- method public void setTitle(java.lang.String);
- method public void startRecognition();
- }
-
- public static abstract interface SearchSupportFragment.SearchResultProvider {
- method public abstract android.support.v17.leanback.widget.ObjectAdapter getResultsAdapter();
- method public abstract boolean onQueryTextChange(java.lang.String);
- method public abstract boolean onQueryTextSubmit(java.lang.String);
- }
-
- public class VerticalGridFragment extends android.support.v17.leanback.app.BaseFragment {
- ctor public VerticalGridFragment();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public android.support.v17.leanback.widget.VerticalGridPresenter getGridPresenter();
- method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setGridPresenter(android.support.v17.leanback.widget.VerticalGridPresenter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- }
-
- public class VerticalGridSupportFragment extends android.support.v17.leanback.app.BaseSupportFragment {
- ctor public VerticalGridSupportFragment();
- method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public android.support.v17.leanback.widget.VerticalGridPresenter getGridPresenter();
- method public android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setGridPresenter(android.support.v17.leanback.widget.VerticalGridPresenter);
- method public void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public void setSelectedPosition(int);
- }
-
- public class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
- ctor public VideoFragment();
- method public android.view.SurfaceView getSurfaceView();
- method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
- }
-
- public class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
- ctor public VideoFragmentGlueHost(android.support.v17.leanback.app.VideoFragment);
- method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
- }
-
- public class VideoSupportFragment extends android.support.v17.leanback.app.PlaybackSupportFragment {
- ctor public VideoSupportFragment();
- method public android.view.SurfaceView getSurfaceView();
- method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
- }
-
- public class VideoSupportFragmentGlueHost extends android.support.v17.leanback.app.PlaybackSupportFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
- ctor public VideoSupportFragmentGlueHost(android.support.v17.leanback.app.VideoSupportFragment);
- method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
- }
-
-}
-
-package android.support.v17.leanback.database {
-
- public abstract class CursorMapper {
- ctor public CursorMapper();
- method protected abstract java.lang.Object bind(android.database.Cursor);
- method protected abstract void bindColumns(android.database.Cursor);
- method public java.lang.Object convert(android.database.Cursor);
- }
-
-}
-
-package android.support.v17.leanback.graphics {
-
- public class BoundsRule {
- ctor public BoundsRule();
- ctor public BoundsRule(android.support.v17.leanback.graphics.BoundsRule);
- method public void calculateBounds(android.graphics.Rect, android.graphics.Rect);
- field public android.support.v17.leanback.graphics.BoundsRule.ValueRule bottom;
- field public android.support.v17.leanback.graphics.BoundsRule.ValueRule left;
- field public android.support.v17.leanback.graphics.BoundsRule.ValueRule right;
- field public android.support.v17.leanback.graphics.BoundsRule.ValueRule top;
- }
-
- public static final class BoundsRule.ValueRule {
- method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule absoluteValue(int);
- method public int getAbsoluteValue();
- method public float getFraction();
- method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParent(float);
- method public static android.support.v17.leanback.graphics.BoundsRule.ValueRule inheritFromParentWithOffset(float, int);
- method public void setAbsoluteValue(int);
- method public void setFraction(float);
- }
-
- public final class ColorFilterCache {
- method public static android.support.v17.leanback.graphics.ColorFilterCache getColorFilterCache(int);
- method public android.graphics.ColorFilter getFilterForLevel(float);
- }
-
- public final class ColorFilterDimmer {
- method public void applyFilterToView(android.view.View);
- method public static android.support.v17.leanback.graphics.ColorFilterDimmer create(android.support.v17.leanback.graphics.ColorFilterCache, float, float);
- method public static android.support.v17.leanback.graphics.ColorFilterDimmer createDefault(android.content.Context);
- method public android.graphics.ColorFilter getColorFilter();
- method public android.graphics.Paint getPaint();
- method public void setActiveLevel(float);
- }
-
- public final class ColorOverlayDimmer {
- method public int applyToColor(int);
- method public static android.support.v17.leanback.graphics.ColorOverlayDimmer createColorOverlayDimmer(int, float, float);
- method public static android.support.v17.leanback.graphics.ColorOverlayDimmer createDefault(android.content.Context);
- method public void drawColorOverlay(android.graphics.Canvas, android.view.View, boolean);
- method public int getAlpha();
- method public float getAlphaFloat();
- method public android.graphics.Paint getPaint();
- method public boolean needsDraw();
- method public void setActiveLevel(float);
- }
-
- public class CompositeDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
- ctor public CompositeDrawable();
- method public void addChildDrawable(android.graphics.drawable.Drawable);
- method public void draw(android.graphics.Canvas);
- method public android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable getChildAt(int);
- method public int getChildCount();
- method public android.graphics.drawable.Drawable getDrawable(int);
- method public int getOpacity();
- method public void invalidateDrawable(android.graphics.drawable.Drawable);
- method public void removeChild(int);
- method public void removeDrawable(android.graphics.drawable.Drawable);
- method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long);
- method public void setAlpha(int);
- method public void setChildDrawableAt(int, android.graphics.drawable.Drawable);
- method public void setColorFilter(android.graphics.ColorFilter);
- method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
- }
-
- public static final class CompositeDrawable.ChildDrawable {
- ctor public CompositeDrawable.ChildDrawable(android.graphics.drawable.Drawable, android.support.v17.leanback.graphics.CompositeDrawable);
- method public android.support.v17.leanback.graphics.BoundsRule getBoundsRule();
- method public android.graphics.drawable.Drawable getDrawable();
- method public void recomputeBounds();
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> BOTTOM_ABSOLUTE;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> BOTTOM_FRACTION;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> LEFT_ABSOLUTE;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> LEFT_FRACTION;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> RIGHT_ABSOLUTE;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> RIGHT_FRACTION;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Integer> TOP_ABSOLUTE;
- field public static final android.util.Property<android.support.v17.leanback.graphics.CompositeDrawable.ChildDrawable, java.lang.Float> TOP_FRACTION;
- }
-
- public class FitWidthBitmapDrawable extends android.graphics.drawable.Drawable {
- ctor public FitWidthBitmapDrawable();
- method public void draw(android.graphics.Canvas);
- method public android.graphics.Bitmap getBitmap();
- method public int getOpacity();
- method public android.graphics.Rect getSource();
- method public int getVerticalOffset();
- method public void setAlpha(int);
- method public void setBitmap(android.graphics.Bitmap);
- method public void setColorFilter(android.graphics.ColorFilter);
- method public void setSource(android.graphics.Rect);
- method public void setVerticalOffset(int);
- field public static final android.util.Property<android.support.v17.leanback.graphics.FitWidthBitmapDrawable, java.lang.Integer> PROPERTY_VERTICAL_OFFSET;
- }
-
-}
-
-package android.support.v17.leanback.media {
-
- public class MediaControllerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
- ctor public MediaControllerAdapter(android.support.v4.media.session.MediaControllerCompat);
- method public android.graphics.drawable.Drawable getMediaArt(android.content.Context);
- method public android.support.v4.media.session.MediaControllerCompat getMediaController();
- method public java.lang.CharSequence getMediaSubtitle();
- method public java.lang.CharSequence getMediaTitle();
- method public void pause();
- method public void play();
- }
-
- public abstract deprecated class MediaControllerGlue extends android.support.v17.leanback.media.PlaybackControlGlue {
- ctor public MediaControllerGlue(android.content.Context, int[], int[]);
- method public void attachToMediaController(android.support.v4.media.session.MediaControllerCompat);
- method public void detach();
- method public int getCurrentPosition();
- method public int getCurrentSpeedId();
- method public android.graphics.drawable.Drawable getMediaArt();
- method public final android.support.v4.media.session.MediaControllerCompat getMediaController();
- method public int getMediaDuration();
- method public java.lang.CharSequence getMediaSubtitle();
- method public java.lang.CharSequence getMediaTitle();
- method public long getSupportedActions();
- method public boolean hasValidMedia();
- method public boolean isMediaPlaying();
- }
-
- public class MediaPlayerAdapter extends android.support.v17.leanback.media.PlayerAdapter {
- ctor public MediaPlayerAdapter(android.content.Context);
- method protected boolean onError(int, int);
- method protected boolean onInfo(int, int);
- method protected void onSeekComplete();
- method public void pause();
- method public void play();
- method public void release();
- method public void reset();
- method public boolean setDataSource(android.net.Uri);
- }
-
- public class PlaybackBannerControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackBaseControlGlue {
- ctor public PlaybackBannerControlGlue(android.content.Context, int[], T);
- ctor public PlaybackBannerControlGlue(android.content.Context, int[], int[], T);
- method public int[] getFastForwardSpeeds();
- method public int[] getRewindSpeeds();
- method public void onActionClicked(android.support.v17.leanback.widget.Action);
- method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
- method public boolean onKey(android.view.View, int, android.view.KeyEvent);
- field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
- field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
- field public static final int ACTION_FAST_FORWARD = 128; // 0x80
- field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
- field public static final int ACTION_REWIND = 32; // 0x20
- field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
- field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
- field public static final int PLAYBACK_SPEED_FAST_L0 = 10; // 0xa
- field public static final int PLAYBACK_SPEED_FAST_L1 = 11; // 0xb
- field public static final int PLAYBACK_SPEED_FAST_L2 = 12; // 0xc
- field public static final int PLAYBACK_SPEED_FAST_L3 = 13; // 0xd
- field public static final int PLAYBACK_SPEED_FAST_L4 = 14; // 0xe
- field public static final int PLAYBACK_SPEED_INVALID = -1; // 0xffffffff
- field public static final int PLAYBACK_SPEED_NORMAL = 1; // 0x1
- field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
- }
-
- public abstract class PlaybackBaseControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
- ctor public PlaybackBaseControlGlue(android.content.Context, T);
- method public android.graphics.drawable.Drawable getArt();
- method public final long getBufferedPosition();
- method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
- method public long getCurrentPosition();
- method public final long getDuration();
- method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
- method public final T getPlayerAdapter();
- method public java.lang.CharSequence getSubtitle();
- method public long getSupportedActions();
- method public java.lang.CharSequence getTitle();
- method public boolean isControlsOverlayAutoHideEnabled();
- method public final boolean isPlaying();
- method public final boolean isPrepared();
- method protected static void notifyItemChanged(android.support.v17.leanback.widget.ArrayObjectAdapter, java.lang.Object);
- method public abstract void onActionClicked(android.support.v17.leanback.widget.Action);
- method protected void onCreatePrimaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
- method protected abstract android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
- method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
- method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
- method protected void onMetadataChanged();
- method protected void onPlayCompleted();
- method protected void onPlayStateChanged();
- method protected void onPreparedStateChanged();
- method protected void onUpdateBufferedProgress();
- method protected void onUpdateDuration();
- method protected void onUpdateProgress();
- method public final void seekTo(long);
- method public void setArt(android.graphics.drawable.Drawable);
- method public void setControlsOverlayAutoHideEnabled(boolean);
- method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
- method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
- method public void setSubtitle(java.lang.CharSequence);
- method public void setTitle(java.lang.CharSequence);
- field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
- field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
- field public static final int ACTION_FAST_FORWARD = 128; // 0x80
- field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
- field public static final int ACTION_REPEAT = 512; // 0x200
- field public static final int ACTION_REWIND = 32; // 0x20
- field public static final int ACTION_SHUFFLE = 1024; // 0x400
- field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
- field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
- }
-
- public abstract class PlaybackControlGlue extends android.support.v17.leanback.media.PlaybackGlue implements android.support.v17.leanback.widget.OnActionClickedListener android.view.View.OnKeyListener {
- ctor public PlaybackControlGlue(android.content.Context, int[]);
- ctor public PlaybackControlGlue(android.content.Context, int[], int[]);
- method public void enableProgressUpdating(boolean);
- method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
- method public deprecated android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
- method public abstract int getCurrentPosition();
- method public abstract int getCurrentSpeedId();
- method public int[] getFastForwardSpeeds();
- method public abstract android.graphics.drawable.Drawable getMediaArt();
- method public abstract int getMediaDuration();
- method public abstract java.lang.CharSequence getMediaSubtitle();
- method public abstract java.lang.CharSequence getMediaTitle();
- method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
- method public int[] getRewindSpeeds();
- method public abstract long getSupportedActions();
- method public int getUpdatePeriod();
- method public abstract boolean hasValidMedia();
- method public boolean isFadingEnabled();
- method public abstract boolean isMediaPlaying();
- method public void onActionClicked(android.support.v17.leanback.widget.Action);
- method protected void onCreateControlsRowAndPresenter();
- method protected void onCreatePrimaryActions(android.support.v17.leanback.widget.SparseArrayObjectAdapter);
- method protected void onCreateSecondaryActions(android.support.v17.leanback.widget.ArrayObjectAdapter);
- method public boolean onKey(android.view.View, int, android.view.KeyEvent);
- method protected void onMetadataChanged();
- method protected void onStateChanged();
- method public void play(int);
- method public final void play();
- method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
- method public deprecated void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
- method public void setFadingEnabled(boolean);
- method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
- method public void updateProgress();
- field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
- field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
- field public static final int ACTION_FAST_FORWARD = 128; // 0x80
- field public static final int ACTION_PLAY_PAUSE = 64; // 0x40
- field public static final int ACTION_REWIND = 32; // 0x20
- field public static final int ACTION_SKIP_TO_NEXT = 256; // 0x100
- field public static final int ACTION_SKIP_TO_PREVIOUS = 16; // 0x10
- field public static final int PLAYBACK_SPEED_FAST_L0 = 10; // 0xa
- field public static final int PLAYBACK_SPEED_FAST_L1 = 11; // 0xb
- field public static final int PLAYBACK_SPEED_FAST_L2 = 12; // 0xc
- field public static final int PLAYBACK_SPEED_FAST_L3 = 13; // 0xd
- field public static final int PLAYBACK_SPEED_FAST_L4 = 14; // 0xe
- field public static final int PLAYBACK_SPEED_INVALID = -1; // 0xffffffff
- field public static final int PLAYBACK_SPEED_NORMAL = 1; // 0x1
- field public static final int PLAYBACK_SPEED_PAUSED = 0; // 0x0
- }
-
- public abstract class PlaybackGlue {
- ctor public PlaybackGlue(android.content.Context);
- method public void addPlayerCallback(android.support.v17.leanback.media.PlaybackGlue.PlayerCallback);
- method public android.content.Context getContext();
- method public android.support.v17.leanback.media.PlaybackGlueHost getHost();
- method protected java.util.List<android.support.v17.leanback.media.PlaybackGlue.PlayerCallback> getPlayerCallbacks();
- method public boolean isPlaying();
- method public boolean isPrepared();
- method public void next();
- method protected void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
- method protected void onDetachedFromHost();
- method protected void onHostPause();
- method protected void onHostResume();
- method protected void onHostStart();
- method protected void onHostStop();
- method public void pause();
- method public void play();
- method public void playWhenPrepared();
- method public void previous();
- method public void removePlayerCallback(android.support.v17.leanback.media.PlaybackGlue.PlayerCallback);
- method public final void setHost(android.support.v17.leanback.media.PlaybackGlueHost);
- }
-
- public static abstract class PlaybackGlue.PlayerCallback {
- ctor public PlaybackGlue.PlayerCallback();
- method public void onPlayCompleted(android.support.v17.leanback.media.PlaybackGlue);
- method public void onPlayStateChanged(android.support.v17.leanback.media.PlaybackGlue);
- method public void onPreparedStateChanged(android.support.v17.leanback.media.PlaybackGlue);
- }
-
- public abstract class PlaybackGlueHost {
- ctor public PlaybackGlueHost();
- method public deprecated void fadeOut();
- method public android.support.v17.leanback.media.PlaybackGlueHost.PlayerCallback getPlayerCallback();
- method public void hideControlsOverlay(boolean);
- method public boolean isControlsOverlayAutoHideEnabled();
- method public boolean isControlsOverlayVisible();
- method public void notifyPlaybackRowChanged();
- method public void setControlsOverlayAutoHideEnabled(boolean);
- method public deprecated void setFadingEnabled(boolean);
- method public void setHostCallback(android.support.v17.leanback.media.PlaybackGlueHost.HostCallback);
- method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
- method public void setOnKeyInterceptListener(android.view.View.OnKeyListener);
- method public void setPlaybackRow(android.support.v17.leanback.widget.Row);
- method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
- method public void showControlsOverlay(boolean);
- }
-
- public static abstract class PlaybackGlueHost.HostCallback {
- ctor public PlaybackGlueHost.HostCallback();
- method public void onHostDestroy();
- method public void onHostPause();
- method public void onHostResume();
- method public void onHostStart();
- method public void onHostStop();
- }
-
- public static class PlaybackGlueHost.PlayerCallback {
- ctor public PlaybackGlueHost.PlayerCallback();
- method public void onBufferingStateChanged(boolean);
- method public void onError(int, java.lang.CharSequence);
- method public void onVideoSizeChanged(int, int);
- }
-
- public class PlaybackTransportControlGlue<T extends android.support.v17.leanback.media.PlayerAdapter> extends android.support.v17.leanback.media.PlaybackBaseControlGlue {
- ctor public PlaybackTransportControlGlue(android.content.Context, T);
- method public final android.support.v17.leanback.widget.PlaybackSeekDataProvider getSeekProvider();
- method public final boolean isSeekEnabled();
- method public void onActionClicked(android.support.v17.leanback.widget.Action);
- method protected android.support.v17.leanback.widget.PlaybackRowPresenter onCreateRowPresenter();
- method public boolean onKey(android.view.View, int, android.view.KeyEvent);
- method public final void setSeekEnabled(boolean);
- method public final void setSeekProvider(android.support.v17.leanback.widget.PlaybackSeekDataProvider);
- }
-
- public abstract class PlayerAdapter {
- ctor public PlayerAdapter();
- method public void fastForward();
- method public long getBufferedPosition();
- method public final android.support.v17.leanback.media.PlayerAdapter.Callback getCallback();
- method public long getCurrentPosition();
- method public long getDuration();
- method public long getSupportedActions();
- method public boolean isPlaying();
- method public boolean isPrepared();
- method public void next();
- method public void onAttachedToHost(android.support.v17.leanback.media.PlaybackGlueHost);
- method public void onDetachedFromHost();
- method public abstract void pause();
- method public abstract void play();
- method public void previous();
- method public void rewind();
- method public void seekTo(long);
- method public final void setCallback(android.support.v17.leanback.media.PlayerAdapter.Callback);
- method public void setProgressUpdatingEnabled(boolean);
- method public void setRepeatAction(int);
- method public void setShuffleAction(int);
- }
-
- public static class PlayerAdapter.Callback {
- ctor public PlayerAdapter.Callback();
- method public void onBufferedPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onBufferingStateChanged(android.support.v17.leanback.media.PlayerAdapter, boolean);
- method public void onCurrentPositionChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onDurationChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onError(android.support.v17.leanback.media.PlayerAdapter, int, java.lang.String);
- method public void onMetadataChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onPlayCompleted(android.support.v17.leanback.media.PlayerAdapter);
- method public void onPlayStateChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onPreparedStateChanged(android.support.v17.leanback.media.PlayerAdapter);
- method public void onVideoSizeChanged(android.support.v17.leanback.media.PlayerAdapter, int, int);
- }
-
- public abstract interface SurfaceHolderGlueHost {
- method public abstract void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
- }
-
-}
-
-package android.support.v17.leanback.system {
-
- public class Settings {
- method public boolean getBoolean(java.lang.String);
- method public static android.support.v17.leanback.system.Settings getInstance(android.content.Context);
- method public void setBoolean(java.lang.String, boolean);
- field public static final java.lang.String OUTLINE_CLIPPING_DISABLED = "OUTLINE_CLIPPING_DISABLED";
- field public static final java.lang.String PREFER_STATIC_SHADOWS = "PREFER_STATIC_SHADOWS";
- }
-
-}
-
-package android.support.v17.leanback.widget {
-
- public abstract class AbstractDetailsDescriptionPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public AbstractDetailsDescriptionPresenter();
- method protected abstract void onBindDescription(android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter.ViewHolder, java.lang.Object);
- method public final void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public final android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- }
-
- public static class AbstractDetailsDescriptionPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
- ctor public AbstractDetailsDescriptionPresenter.ViewHolder(android.view.View);
- method public android.widget.TextView getBody();
- method public android.widget.TextView getSubtitle();
- method public android.widget.TextView getTitle();
- }
-
- public abstract class AbstractMediaItemPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public AbstractMediaItemPresenter();
- ctor public AbstractMediaItemPresenter(int);
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method public android.support.v17.leanback.widget.Presenter getActionPresenter();
- method protected int getMediaPlayState(java.lang.Object);
- method public int getThemeId();
- method public boolean hasMediaRowSeparator();
- method protected abstract void onBindMediaDetails(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder, java.lang.Object);
- method public void onBindMediaPlayState(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
- method protected void onBindRowActions(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
- method protected void onUnbindMediaDetails(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
- method public void onUnbindMediaPlayState(android.support.v17.leanback.widget.AbstractMediaItemPresenter.ViewHolder);
- method public void setActionPresenter(android.support.v17.leanback.widget.Presenter);
- method public void setBackgroundColor(int);
- method public void setHasMediaRowSeparator(boolean);
- method public void setThemeId(int);
- field public static final int PLAY_STATE_INITIAL = 0; // 0x0
- field public static final int PLAY_STATE_PAUSED = 1; // 0x1
- field public static final int PLAY_STATE_PLAYING = 2; // 0x2
- }
-
- public static class AbstractMediaItemPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public AbstractMediaItemPresenter.ViewHolder(android.view.View);
- method public android.view.ViewGroup getMediaItemActionsContainer();
- method public android.view.View getMediaItemDetailsView();
- method public android.widget.TextView getMediaItemDurationView();
- method public android.widget.TextView getMediaItemNameView();
- method public android.widget.TextView getMediaItemNumberView();
- method public android.widget.ViewFlipper getMediaItemNumberViewFlipper();
- method public android.view.View getMediaItemPausedView();
- method public android.view.View getMediaItemPlayingView();
- method public android.support.v17.leanback.widget.MultiActionsProvider.MultiAction[] getMediaItemRowActions();
- method public android.view.View getMediaItemRowSeparator();
- method public android.view.View getSelectorView();
- method public void notifyActionChanged(android.support.v17.leanback.widget.MultiActionsProvider.MultiAction);
- method public void notifyDetailsChanged();
- method public void notifyPlayStateChanged();
- method public void onBindRowActions();
- method public void setSelectedMediaItemNumberView(int);
- }
-
- public abstract class AbstractMediaListHeaderPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public AbstractMediaListHeaderPresenter(android.content.Context, int);
- ctor public AbstractMediaListHeaderPresenter();
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method protected abstract void onBindMediaListHeaderViewHolder(android.support.v17.leanback.widget.AbstractMediaListHeaderPresenter.ViewHolder, java.lang.Object);
- method public void setBackgroundColor(int);
- }
-
- public static class AbstractMediaListHeaderPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public AbstractMediaListHeaderPresenter.ViewHolder(android.view.View);
- method public android.widget.TextView getHeaderView();
- }
-
- public class Action {
- ctor public Action(long);
- ctor public Action(long, java.lang.CharSequence);
- ctor public Action(long, java.lang.CharSequence, java.lang.CharSequence);
- ctor public Action(long, java.lang.CharSequence, java.lang.CharSequence, android.graphics.drawable.Drawable);
- method public final void addKeyCode(int);
- method public final android.graphics.drawable.Drawable getIcon();
- method public final long getId();
- method public final java.lang.CharSequence getLabel1();
- method public final java.lang.CharSequence getLabel2();
- method public final void removeKeyCode(int);
- method public final boolean respondsToKeyCode(int);
- method public final void setIcon(android.graphics.drawable.Drawable);
- method public final void setId(long);
- method public final void setLabel1(java.lang.CharSequence);
- method public final void setLabel2(java.lang.CharSequence);
- field public static final long NO_ID = -1L; // 0xffffffffffffffffL
- }
-
- public class ArrayObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
- ctor public ArrayObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
- ctor public ArrayObjectAdapter(android.support.v17.leanback.widget.Presenter);
- ctor public ArrayObjectAdapter();
- method public void add(java.lang.Object);
- method public void add(int, java.lang.Object);
- method public void addAll(int, java.util.Collection);
- method public void clear();
- method public java.lang.Object get(int);
- method public int indexOf(java.lang.Object);
- method public void move(int, int);
- method public void notifyArrayItemRangeChanged(int, int);
- method public boolean remove(java.lang.Object);
- method public int removeItems(int, int);
- method public void replace(int, java.lang.Object);
- method public void setItems(java.util.List, android.support.v17.leanback.widget.DiffCallback);
- method public int size();
- method public <E> java.util.List<E> unmodifiableList();
- }
-
- public class BaseCardView extends android.widget.FrameLayout {
- ctor public BaseCardView(android.content.Context);
- ctor public BaseCardView(android.content.Context, android.util.AttributeSet);
- ctor public BaseCardView(android.content.Context, android.util.AttributeSet, int);
- method protected android.support.v17.leanback.widget.BaseCardView.LayoutParams generateDefaultLayoutParams();
- method public android.support.v17.leanback.widget.BaseCardView.LayoutParams generateLayoutParams(android.util.AttributeSet);
- method protected android.support.v17.leanback.widget.BaseCardView.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
- method public int getCardType();
- method public deprecated int getExtraVisibility();
- method public int getInfoVisibility();
- method public boolean isSelectedAnimationDelayed();
- method public void setCardType(int);
- method public deprecated void setExtraVisibility(int);
- method public void setInfoVisibility(int);
- method public void setSelectedAnimationDelayed(boolean);
- field public static final int CARD_REGION_VISIBLE_ACTIVATED = 1; // 0x1
- field public static final int CARD_REGION_VISIBLE_ALWAYS = 0; // 0x0
- field public static final int CARD_REGION_VISIBLE_SELECTED = 2; // 0x2
- field public static final int CARD_TYPE_INFO_OVER = 1; // 0x1
- field public static final int CARD_TYPE_INFO_UNDER = 2; // 0x2
- field public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3; // 0x3
- field public static final int CARD_TYPE_MAIN_ONLY = 0; // 0x0
- }
-
- public static class BaseCardView.LayoutParams extends android.widget.FrameLayout.LayoutParams {
- ctor public BaseCardView.LayoutParams(android.content.Context, android.util.AttributeSet);
- ctor public BaseCardView.LayoutParams(int, int);
- ctor public BaseCardView.LayoutParams(android.view.ViewGroup.LayoutParams);
- ctor public BaseCardView.LayoutParams(android.support.v17.leanback.widget.BaseCardView.LayoutParams);
- field public static final int VIEW_TYPE_EXTRA = 2; // 0x2
- field public static final int VIEW_TYPE_INFO = 1; // 0x1
- field public static final int VIEW_TYPE_MAIN = 0; // 0x0
- field public int viewType;
- }
-
- public abstract class BaseGridView extends android.support.v7.widget.RecyclerView {
- method public void addOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
- method public void animateIn();
- method public void animateOut();
- method public int getChildDrawingOrder(int, int);
- method public deprecated int getHorizontalMargin();
- method public int getHorizontalSpacing();
- method public int getInitialPrefetchItemCount();
- method public int getItemAlignmentOffset();
- method public float getItemAlignmentOffsetPercent();
- method public int getItemAlignmentViewId();
- method public android.support.v17.leanback.widget.BaseGridView.OnUnhandledKeyListener getOnUnhandledKeyListener();
- method public final int getSaveChildrenLimitNumber();
- method public final int getSaveChildrenPolicy();
- method public int getSelectedPosition();
- method public deprecated int getVerticalMargin();
- method public int getVerticalSpacing();
- method public void getViewSelectedOffsets(android.view.View, int[]);
- method public int getWindowAlignment();
- method public int getWindowAlignmentOffset();
- method public float getWindowAlignmentOffsetPercent();
- method public boolean hasPreviousViewInSameRow(int);
- method public boolean isChildLayoutAnimated();
- method public boolean isFocusDrawingOrderEnabled();
- method public final boolean isFocusSearchDisabled();
- method public boolean isItemAlignmentOffsetWithPadding();
- method public boolean isScrollEnabled();
- method public boolean isWindowAlignmentPreferKeyLineOverHighEdge();
- method public boolean isWindowAlignmentPreferKeyLineOverLowEdge();
- method public boolean onRequestFocusInDescendants(int, android.graphics.Rect);
- method public void removeOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
- method public void setAnimateChildLayout(boolean);
- method public void setChildrenVisibility(int);
- method public void setFocusDrawingOrderEnabled(boolean);
- method public final void setFocusSearchDisabled(boolean);
- method public void setGravity(int);
- method public void setHasOverlappingRendering(boolean);
- method public deprecated void setHorizontalMargin(int);
- method public void setHorizontalSpacing(int);
- method public void setInitialPrefetchItemCount(int);
- method public void setItemAlignmentOffset(int);
- method public void setItemAlignmentOffsetPercent(float);
- method public void setItemAlignmentOffsetWithPadding(boolean);
- method public void setItemAlignmentViewId(int);
- method public deprecated void setItemMargin(int);
- method public void setItemSpacing(int);
- method public void setLayoutEnabled(boolean);
- method public void setOnChildLaidOutListener(android.support.v17.leanback.widget.OnChildLaidOutListener);
- method public void setOnChildSelectedListener(android.support.v17.leanback.widget.OnChildSelectedListener);
- method public void setOnChildViewHolderSelectedListener(android.support.v17.leanback.widget.OnChildViewHolderSelectedListener);
- method public void setOnKeyInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnKeyInterceptListener);
- method public void setOnMotionInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnMotionInterceptListener);
- method public void setOnTouchInterceptListener(android.support.v17.leanback.widget.BaseGridView.OnTouchInterceptListener);
- method public void setOnUnhandledKeyListener(android.support.v17.leanback.widget.BaseGridView.OnUnhandledKeyListener);
- method public void setPruneChild(boolean);
- method public final void setSaveChildrenLimitNumber(int);
- method public final void setSaveChildrenPolicy(int);
- method public void setScrollEnabled(boolean);
- method public void setSelectedPosition(int);
- method public void setSelectedPosition(int, int);
- method public void setSelectedPosition(int, android.support.v17.leanback.widget.ViewHolderTask);
- method public void setSelectedPositionSmooth(int);
- method public void setSelectedPositionSmooth(int, android.support.v17.leanback.widget.ViewHolderTask);
- method public deprecated void setVerticalMargin(int);
- method public void setVerticalSpacing(int);
- method public void setWindowAlignment(int);
- method public void setWindowAlignmentOffset(int);
- method public void setWindowAlignmentOffsetPercent(float);
- method public void setWindowAlignmentPreferKeyLineOverHighEdge(boolean);
- method public void setWindowAlignmentPreferKeyLineOverLowEdge(boolean);
- field public static final float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
- field public static final int SAVE_ALL_CHILD = 3; // 0x3
- field public static final int SAVE_LIMITED_CHILD = 2; // 0x2
- field public static final int SAVE_NO_CHILD = 0; // 0x0
- field public static final int SAVE_ON_SCREEN_CHILD = 1; // 0x1
- field public static final int WINDOW_ALIGN_BOTH_EDGE = 3; // 0x3
- field public static final int WINDOW_ALIGN_HIGH_EDGE = 2; // 0x2
- field public static final int WINDOW_ALIGN_LOW_EDGE = 1; // 0x1
- field public static final int WINDOW_ALIGN_NO_EDGE = 0; // 0x0
- field public static final float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
- }
-
- public static abstract interface BaseGridView.OnKeyInterceptListener {
- method public abstract boolean onInterceptKeyEvent(android.view.KeyEvent);
- }
-
- public static abstract interface BaseGridView.OnMotionInterceptListener {
- method public abstract boolean onInterceptMotionEvent(android.view.MotionEvent);
- }
-
- public static abstract interface BaseGridView.OnTouchInterceptListener {
- method public abstract boolean onInterceptTouchEvent(android.view.MotionEvent);
- }
-
- public static abstract interface BaseGridView.OnUnhandledKeyListener {
- method public abstract boolean onUnhandledKey(android.view.KeyEvent);
- }
-
- public abstract interface BaseOnItemViewClickedListener<T> {
- method public abstract void onItemClicked(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, android.support.v17.leanback.widget.RowPresenter.ViewHolder, T);
- }
-
- public abstract interface BaseOnItemViewSelectedListener<T> {
- method public abstract void onItemSelected(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, android.support.v17.leanback.widget.RowPresenter.ViewHolder, T);
- }
-
- public class BrowseFrameLayout extends android.widget.FrameLayout {
- ctor public BrowseFrameLayout(android.content.Context);
- ctor public BrowseFrameLayout(android.content.Context, android.util.AttributeSet);
- ctor public BrowseFrameLayout(android.content.Context, android.util.AttributeSet, int);
- method public android.support.v17.leanback.widget.BrowseFrameLayout.OnChildFocusListener getOnChildFocusListener();
- method public android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener getOnFocusSearchListener();
- method public void setOnChildFocusListener(android.support.v17.leanback.widget.BrowseFrameLayout.OnChildFocusListener);
- method public void setOnDispatchKeyListener(android.view.View.OnKeyListener);
- method public void setOnFocusSearchListener(android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener);
- }
-
- public static abstract interface BrowseFrameLayout.OnChildFocusListener {
- method public abstract void onRequestChildFocus(android.view.View, android.view.View);
- method public abstract boolean onRequestFocusInDescendants(int, android.graphics.Rect);
- }
-
- public static abstract interface BrowseFrameLayout.OnFocusSearchListener {
- method public abstract android.view.View onFocusSearch(android.view.View, int);
- }
-
- public final class ClassPresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
- ctor public ClassPresenterSelector();
- method public android.support.v17.leanback.widget.ClassPresenterSelector addClassPresenter(java.lang.Class<?>, android.support.v17.leanback.widget.Presenter);
- method public android.support.v17.leanback.widget.ClassPresenterSelector addClassPresenterSelector(java.lang.Class<?>, android.support.v17.leanback.widget.PresenterSelector);
- method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
- }
-
- public class ControlButtonPresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
- ctor public ControlButtonPresenterSelector();
- method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
- method public android.support.v17.leanback.widget.Presenter getPrimaryPresenter();
- method public android.support.v17.leanback.widget.Presenter getSecondaryPresenter();
- }
-
- public class CursorObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
- ctor public CursorObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
- ctor public CursorObjectAdapter(android.support.v17.leanback.widget.Presenter);
- ctor public CursorObjectAdapter();
- method public void changeCursor(android.database.Cursor);
- method public void close();
- method public java.lang.Object get(int);
- method public final android.database.Cursor getCursor();
- method public final android.support.v17.leanback.database.CursorMapper getMapper();
- method protected final void invalidateCache(int);
- method protected final void invalidateCache(int, int);
- method public boolean isClosed();
- method protected void onCursorChanged();
- method protected void onMapperChanged();
- method public final void setMapper(android.support.v17.leanback.database.CursorMapper);
- method public int size();
- method public android.database.Cursor swapCursor(android.database.Cursor);
- }
-
- public class DetailsOverviewLogoPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public DetailsOverviewLogoPresenter();
- method public boolean isBoundToImage(android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder, android.support.v17.leanback.widget.DetailsOverviewRow);
- method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public android.view.View onCreateView(android.view.ViewGroup);
- method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void setContext(android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter);
- }
-
- public static class DetailsOverviewLogoPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
- ctor public DetailsOverviewLogoPresenter.ViewHolder(android.view.View);
- method public android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter getParentPresenter();
- method public android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder getParentViewHolder();
- method public boolean isSizeFromDrawableIntrinsic();
- method public void setSizeFromDrawableIntrinsic(boolean);
- field protected android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter mParentPresenter;
- field protected android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder mParentViewHolder;
- }
-
- public class DetailsOverviewRow extends android.support.v17.leanback.widget.Row {
- ctor public DetailsOverviewRow(java.lang.Object);
- method public final deprecated void addAction(android.support.v17.leanback.widget.Action);
- method public final deprecated void addAction(int, android.support.v17.leanback.widget.Action);
- method public android.support.v17.leanback.widget.Action getActionForKeyCode(int);
- method public final deprecated java.util.List<android.support.v17.leanback.widget.Action> getActions();
- method public final android.support.v17.leanback.widget.ObjectAdapter getActionsAdapter();
- method public final android.graphics.drawable.Drawable getImageDrawable();
- method public final java.lang.Object getItem();
- method public boolean isImageScaleUpAllowed();
- method public final deprecated boolean removeAction(android.support.v17.leanback.widget.Action);
- method public final void setActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public final void setImageBitmap(android.content.Context, android.graphics.Bitmap);
- method public final void setImageDrawable(android.graphics.drawable.Drawable);
- method public void setImageScaleUpAllowed(boolean);
- method public final void setItem(java.lang.Object);
- }
-
- public static class DetailsOverviewRow.Listener {
- ctor public DetailsOverviewRow.Listener();
- method public void onActionsAdapterChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
- method public void onImageDrawableChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
- method public void onItemChanged(android.support.v17.leanback.widget.DetailsOverviewRow);
- }
-
- public deprecated class DetailsOverviewRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public DetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter);
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method public int getBackgroundColor();
- method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
- method public boolean isStyleLarge();
- method public final boolean isUsingDefaultSelectEffect();
- method public void setBackgroundColor(int);
- method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
- method public final void setSharedElementEnterTransition(android.app.Activity, java.lang.String, long);
- method public final void setSharedElementEnterTransition(android.app.Activity, java.lang.String);
- method public void setStyleLarge(boolean);
- }
-
- public final class DetailsOverviewRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public DetailsOverviewRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter);
- field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDetailsDescriptionViewHolder;
- }
-
- public class DetailsParallax extends android.support.v17.leanback.widget.RecyclerViewParallax {
- ctor public DetailsParallax();
- method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowBottom();
- method public android.support.v17.leanback.widget.Parallax.IntProperty getOverviewRowTop();
- }
-
- public abstract class DiffCallback<Value> {
- ctor public DiffCallback();
- method public abstract boolean areContentsTheSame(Value, Value);
- method public abstract boolean areItemsTheSame(Value, Value);
- method public java.lang.Object getChangePayload(Value, Value);
- }
-
- public class DividerPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public DividerPresenter();
- method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- }
-
- public class DividerRow extends android.support.v17.leanback.widget.Row {
- ctor public DividerRow();
- method public final boolean isRenderedAsRowView();
- }
-
- public abstract interface FacetProvider {
- method public abstract java.lang.Object getFacet(java.lang.Class<?>);
- }
-
- public abstract interface FacetProviderAdapter {
- method public abstract android.support.v17.leanback.widget.FacetProvider getFacetProvider(int);
- }
-
- public abstract interface FocusHighlight {
- field public static final int ZOOM_FACTOR_LARGE = 3; // 0x3
- field public static final int ZOOM_FACTOR_MEDIUM = 2; // 0x2
- field public static final int ZOOM_FACTOR_NONE = 0; // 0x0
- field public static final int ZOOM_FACTOR_SMALL = 1; // 0x1
- field public static final int ZOOM_FACTOR_XSMALL = 4; // 0x4
- }
-
- public class FocusHighlightHelper {
- ctor public FocusHighlightHelper();
- method public static void setupBrowseItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter, int, boolean);
- method public static deprecated void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView);
- method public static deprecated void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.VerticalGridView, boolean);
- method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter);
- method public static void setupHeaderItemFocusHighlight(android.support.v17.leanback.widget.ItemBridgeAdapter, boolean);
- }
-
- public abstract interface FragmentAnimationProvider {
- method public abstract void onImeAppearing(java.util.List<android.animation.Animator>);
- method public abstract void onImeDisappearing(java.util.List<android.animation.Animator>);
- }
-
- public class FullWidthDetailsOverviewRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public FullWidthDetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter);
- ctor public FullWidthDetailsOverviewRowPresenter(android.support.v17.leanback.widget.Presenter, android.support.v17.leanback.widget.DetailsOverviewLogoPresenter);
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method public final int getActionsBackgroundColor();
- method public final int getAlignmentMode();
- method public final int getBackgroundColor();
- method public final int getInitialState();
- method protected int getLayoutResourceId();
- method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
- method public final boolean isParticipatingEntranceTransition();
- method public final boolean isUsingDefaultSelectEffect();
- method public final void notifyOnBindLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder);
- method protected void onLayoutLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, boolean);
- method protected void onLayoutOverviewFrame(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int, boolean);
- method protected void onStateChanged(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int);
- method public final void setActionsBackgroundColor(int);
- method public final void setAlignmentMode(int);
- method public final void setBackgroundColor(int);
- method public final void setInitialState(int);
- method public final void setListener(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.Listener);
- method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
- method public final void setParticipatingEntranceTransition(boolean);
- method public final void setState(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder, int);
- field public static final int ALIGN_MODE_MIDDLE = 1; // 0x1
- field public static final int ALIGN_MODE_START = 0; // 0x0
- field public static final int STATE_FULL = 1; // 0x1
- field public static final int STATE_HALF = 0; // 0x0
- field public static final int STATE_SMALL = 2; // 0x2
- field protected int mInitialState;
- }
-
- public static abstract class FullWidthDetailsOverviewRowPresenter.Listener {
- ctor public FullWidthDetailsOverviewRowPresenter.Listener();
- method public void onBindLogo(android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.ViewHolder);
- }
-
- public class FullWidthDetailsOverviewRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public FullWidthDetailsOverviewRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter, android.support.v17.leanback.widget.DetailsOverviewLogoPresenter);
- method protected android.support.v17.leanback.widget.DetailsOverviewRow.Listener createRowListener();
- method public final android.view.ViewGroup getActionsRow();
- method public final android.view.ViewGroup getDetailsDescriptionFrame();
- method public final android.support.v17.leanback.widget.Presenter.ViewHolder getDetailsDescriptionViewHolder();
- method public final android.support.v17.leanback.widget.DetailsOverviewLogoPresenter.ViewHolder getLogoViewHolder();
- method public final android.view.ViewGroup getOverviewView();
- method public final int getState();
- field protected final android.support.v17.leanback.widget.DetailsOverviewRow.Listener mRowListener;
- }
-
- public class FullWidthDetailsOverviewRowPresenter.ViewHolder.DetailsOverviewRowListener extends android.support.v17.leanback.widget.DetailsOverviewRow.Listener {
- ctor public FullWidthDetailsOverviewRowPresenter.ViewHolder.DetailsOverviewRowListener();
- }
-
- public class FullWidthDetailsOverviewSharedElementHelper extends android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter.Listener {
- ctor public FullWidthDetailsOverviewSharedElementHelper();
- method public boolean getAutoStartSharedElementTransition();
- method public void setAutoStartSharedElementTransition(boolean);
- method public void setSharedElementEnterTransition(android.app.Activity, java.lang.String);
- method public void setSharedElementEnterTransition(android.app.Activity, java.lang.String, long);
- method public void startPostponedEnterTransition();
- }
-
- public class GuidanceStylist implements android.support.v17.leanback.widget.FragmentAnimationProvider {
- ctor public GuidanceStylist();
- method public android.widget.TextView getBreadcrumbView();
- method public android.widget.TextView getDescriptionView();
- method public android.widget.ImageView getIconView();
- method public android.widget.TextView getTitleView();
- method public android.view.View onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.support.v17.leanback.widget.GuidanceStylist.Guidance);
- method public void onDestroyView();
- method public void onImeAppearing(java.util.List<android.animation.Animator>);
- method public void onImeDisappearing(java.util.List<android.animation.Animator>);
- method public int onProvideLayoutId();
- }
-
- public static class GuidanceStylist.Guidance {
- ctor public GuidanceStylist.Guidance(java.lang.String, java.lang.String, java.lang.String, android.graphics.drawable.Drawable);
- method public java.lang.String getBreadcrumb();
- method public java.lang.String getDescription();
- method public android.graphics.drawable.Drawable getIconDrawable();
- method public java.lang.String getTitle();
- }
-
- public class GuidedAction extends android.support.v17.leanback.widget.Action {
- ctor protected GuidedAction();
- method public int getCheckSetId();
- method public java.lang.CharSequence getDescription();
- method public int getDescriptionEditInputType();
- method public int getDescriptionInputType();
- method public java.lang.CharSequence getEditDescription();
- method public int getEditInputType();
- method public java.lang.CharSequence getEditTitle();
- method public int getInputType();
- method public android.content.Intent getIntent();
- method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getSubActions();
- method public java.lang.CharSequence getTitle();
- method public boolean hasEditableActivatorView();
- method public boolean hasMultilineDescription();
- method public boolean hasNext();
- method public boolean hasSubActions();
- method public boolean hasTextEditable();
- method public boolean infoOnly();
- method public final boolean isAutoSaveRestoreEnabled();
- method public boolean isChecked();
- method public boolean isDescriptionEditable();
- method public boolean isEditTitleUsed();
- method public boolean isEditable();
- method public boolean isEnabled();
- method public boolean isFocusable();
- method public void onRestoreInstanceState(android.os.Bundle, java.lang.String);
- method public void onSaveInstanceState(android.os.Bundle, java.lang.String);
- method public void setChecked(boolean);
- method public void setDescription(java.lang.CharSequence);
- method public void setEditDescription(java.lang.CharSequence);
- method public void setEditTitle(java.lang.CharSequence);
- method public void setEnabled(boolean);
- method public void setFocusable(boolean);
- method public void setIntent(android.content.Intent);
- method public void setSubActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public void setTitle(java.lang.CharSequence);
- field public static final long ACTION_ID_CANCEL = -5L; // 0xfffffffffffffffbL
- field public static final long ACTION_ID_CONTINUE = -7L; // 0xfffffffffffffff9L
- field public static final long ACTION_ID_CURRENT = -3L; // 0xfffffffffffffffdL
- field public static final long ACTION_ID_FINISH = -6L; // 0xfffffffffffffffaL
- field public static final long ACTION_ID_NEXT = -2L; // 0xfffffffffffffffeL
- field public static final long ACTION_ID_NO = -9L; // 0xfffffffffffffff7L
- field public static final long ACTION_ID_OK = -4L; // 0xfffffffffffffffcL
- field public static final long ACTION_ID_YES = -8L; // 0xfffffffffffffff8L
- field public static final int CHECKBOX_CHECK_SET_ID = -1; // 0xffffffff
- field public static final int DEFAULT_CHECK_SET_ID = 1; // 0x1
- field public static final int NO_CHECK_SET = 0; // 0x0
- }
-
- public static class GuidedAction.Builder extends android.support.v17.leanback.widget.GuidedAction.BuilderBase {
- ctor public deprecated GuidedAction.Builder();
- ctor public GuidedAction.Builder(android.content.Context);
- method public android.support.v17.leanback.widget.GuidedAction build();
- }
-
- public static abstract class GuidedAction.BuilderBase<B extends android.support.v17.leanback.widget.GuidedAction.BuilderBase> {
- ctor public GuidedAction.BuilderBase(android.content.Context);
- method protected final void applyValues(android.support.v17.leanback.widget.GuidedAction);
- method public B autoSaveRestoreEnabled(boolean);
- method public B checkSetId(int);
- method public B checked(boolean);
- method public B clickAction(long);
- method public B description(java.lang.CharSequence);
- method public B description(int);
- method public B descriptionEditInputType(int);
- method public B descriptionEditable(boolean);
- method public B descriptionInputType(int);
- method public B editDescription(java.lang.CharSequence);
- method public B editDescription(int);
- method public B editInputType(int);
- method public B editTitle(java.lang.CharSequence);
- method public B editTitle(int);
- method public B editable(boolean);
- method public B enabled(boolean);
- method public B focusable(boolean);
- method public android.content.Context getContext();
- method public B hasEditableActivatorView(boolean);
- method public B hasNext(boolean);
- method public B icon(android.graphics.drawable.Drawable);
- method public B icon(int);
- method public deprecated B iconResourceId(int, android.content.Context);
- method public B id(long);
- method public B infoOnly(boolean);
- method public B inputType(int);
- method public B intent(android.content.Intent);
- method public B multilineDescription(boolean);
- method public B subActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public B title(java.lang.CharSequence);
- method public B title(int);
- }
-
- public class GuidedActionEditText extends android.widget.EditText implements android.support.v17.leanback.widget.ImeKeyMonitor {
- ctor public GuidedActionEditText(android.content.Context);
- ctor public GuidedActionEditText(android.content.Context, android.util.AttributeSet);
- ctor public GuidedActionEditText(android.content.Context, android.util.AttributeSet, int);
- method public void setImeKeyListener(android.support.v17.leanback.widget.ImeKeyMonitor.ImeKeyListener);
- }
-
- public class GuidedActionsStylist implements android.support.v17.leanback.widget.FragmentAnimationProvider {
- ctor public GuidedActionsStylist();
- method public void collapseAction(boolean);
- method public void expandAction(android.support.v17.leanback.widget.GuidedAction, boolean);
- method public android.support.v17.leanback.widget.VerticalGridView getActionsGridView();
- method public android.support.v17.leanback.widget.GuidedAction getExpandedAction();
- method public int getItemViewType(android.support.v17.leanback.widget.GuidedAction);
- method public android.support.v17.leanback.widget.VerticalGridView getSubActionsGridView();
- method public final boolean isBackKeyToCollapseActivatorView();
- method public final boolean isBackKeyToCollapseSubActions();
- method public boolean isButtonActions();
- method public boolean isExpandTransitionSupported();
- method public boolean isExpanded();
- method public boolean isInExpandTransition();
- method public boolean isSubActionsExpanded();
- method public void onAnimateItemChecked(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
- method public void onAnimateItemFocused(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
- method public void onAnimateItemPressed(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
- method public void onAnimateItemPressedCancelled(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
- method public void onBindActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public void onBindCheckMarkView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public void onBindChevronView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public void onBindViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public android.view.View onCreateView(android.view.LayoutInflater, android.view.ViewGroup);
- method public android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder onCreateViewHolder(android.view.ViewGroup, int);
- method public void onDestroyView();
- method protected deprecated void onEditingModeChange(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction, boolean);
- method protected void onEditingModeChange(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean, boolean);
- method public void onImeAppearing(java.util.List<android.animation.Animator>);
- method public void onImeDisappearing(java.util.List<android.animation.Animator>);
- method public int onProvideItemLayoutId();
- method public int onProvideItemLayoutId(int);
- method public int onProvideLayoutId();
- method public boolean onUpdateActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public void onUpdateExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
- method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
- method public void setAsButtonActions();
- method public final void setBackKeyToCollapseActivatorView(boolean);
- method public final void setBackKeyToCollapseSubActions(boolean);
- method public deprecated void setEditingMode(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction, boolean);
- method public deprecated void setExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
- method protected void setupImeOptions(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
- method public deprecated void startExpandedTransition(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
- field public static final int VIEW_TYPE_DATE_PICKER = 1; // 0x1
- field public static final int VIEW_TYPE_DEFAULT = 0; // 0x0
- }
-
- public static class GuidedActionsStylist.ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
- ctor public GuidedActionsStylist.ViewHolder(android.view.View);
- ctor public GuidedActionsStylist.ViewHolder(android.view.View, boolean);
- method public android.support.v17.leanback.widget.GuidedAction getAction();
- method public android.widget.ImageView getCheckmarkView();
- method public android.widget.ImageView getChevronView();
- method public android.view.View getContentView();
- method public android.widget.TextView getDescriptionView();
- method public android.widget.EditText getEditableDescriptionView();
- method public android.widget.EditText getEditableTitleView();
- method public android.view.View getEditingView();
- method public java.lang.Object getFacet(java.lang.Class<?>);
- method public android.widget.ImageView getIconView();
- method public android.widget.TextView getTitleView();
- method public boolean isInEditing();
- method public boolean isInEditingActivatorView();
- method public boolean isInEditingDescription();
- method public boolean isInEditingText();
- method public boolean isInEditingTitle();
- method public boolean isSubAction();
- }
-
- public class GuidedDatePickerAction extends android.support.v17.leanback.widget.GuidedAction {
- ctor public GuidedDatePickerAction();
- method public long getDate();
- method public java.lang.String getDatePickerFormat();
- method public long getMaxDate();
- method public long getMinDate();
- method public void setDate(long);
- }
-
- public static final class GuidedDatePickerAction.Builder extends android.support.v17.leanback.widget.GuidedDatePickerAction.BuilderBase {
- ctor public GuidedDatePickerAction.Builder(android.content.Context);
- method public android.support.v17.leanback.widget.GuidedDatePickerAction build();
- }
-
- public static abstract class GuidedDatePickerAction.BuilderBase<B extends android.support.v17.leanback.widget.GuidedDatePickerAction.BuilderBase> extends android.support.v17.leanback.widget.GuidedAction.BuilderBase {
- ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
- method protected final void applyDatePickerValues(android.support.v17.leanback.widget.GuidedDatePickerAction);
- method public B date(long);
- method public B datePickerFormat(java.lang.String);
- method public B maxDate(long);
- method public B minDate(long);
- }
-
- public class HeaderItem {
- ctor public HeaderItem(long, java.lang.String);
- ctor public HeaderItem(java.lang.String);
- method public java.lang.CharSequence getContentDescription();
- method public java.lang.CharSequence getDescription();
- method public final long getId();
- method public final java.lang.String getName();
- method public void setContentDescription(java.lang.CharSequence);
- method public void setDescription(java.lang.CharSequence);
- }
-
- public class HorizontalGridView extends android.support.v17.leanback.widget.BaseGridView {
- ctor public HorizontalGridView(android.content.Context);
- ctor public HorizontalGridView(android.content.Context, android.util.AttributeSet);
- ctor public HorizontalGridView(android.content.Context, android.util.AttributeSet, int);
- method public final boolean getFadingLeftEdge();
- method public final int getFadingLeftEdgeLength();
- method public final int getFadingLeftEdgeOffset();
- method public final boolean getFadingRightEdge();
- method public final int getFadingRightEdgeLength();
- method public final int getFadingRightEdgeOffset();
- method protected void initAttributes(android.content.Context, android.util.AttributeSet);
- method public final void setFadingLeftEdge(boolean);
- method public final void setFadingLeftEdgeLength(int);
- method public final void setFadingLeftEdgeOffset(int);
- method public final void setFadingRightEdge(boolean);
- method public final void setFadingRightEdgeLength(int);
- method public final void setFadingRightEdgeOffset(int);
- method public void setNumRows(int);
- method public void setRowHeight(int);
- }
-
- public final class HorizontalHoverCardSwitcher extends android.support.v17.leanback.widget.PresenterSwitcher {
- ctor public HorizontalHoverCardSwitcher();
- method protected void insertView(android.view.View);
- method public void select(android.support.v17.leanback.widget.HorizontalGridView, android.view.View, java.lang.Object);
- }
-
- public class ImageCardView extends android.support.v17.leanback.widget.BaseCardView {
- ctor public deprecated ImageCardView(android.content.Context, int);
- ctor public ImageCardView(android.content.Context, android.util.AttributeSet, int);
- ctor public ImageCardView(android.content.Context);
- ctor public ImageCardView(android.content.Context, android.util.AttributeSet);
- method public android.graphics.drawable.Drawable getBadgeImage();
- method public java.lang.CharSequence getContentText();
- method public android.graphics.drawable.Drawable getInfoAreaBackground();
- method public android.graphics.drawable.Drawable getMainImage();
- method public final android.widget.ImageView getMainImageView();
- method public java.lang.CharSequence getTitleText();
- method public void setBadgeImage(android.graphics.drawable.Drawable);
- method public void setContentText(java.lang.CharSequence);
- method public void setInfoAreaBackground(android.graphics.drawable.Drawable);
- method public void setInfoAreaBackgroundColor(int);
- method public void setMainImage(android.graphics.drawable.Drawable);
- method public void setMainImage(android.graphics.drawable.Drawable, boolean);
- method public void setMainImageAdjustViewBounds(boolean);
- method public void setMainImageDimensions(int, int);
- method public void setMainImageScaleType(android.widget.ImageView.ScaleType);
- method public void setTitleText(java.lang.CharSequence);
- field public static final int CARD_TYPE_FLAG_CONTENT = 2; // 0x2
- field public static final int CARD_TYPE_FLAG_ICON_LEFT = 8; // 0x8
- field public static final int CARD_TYPE_FLAG_ICON_RIGHT = 4; // 0x4
- field public static final int CARD_TYPE_FLAG_IMAGE_ONLY = 0; // 0x0
- field public static final int CARD_TYPE_FLAG_TITLE = 1; // 0x1
- }
-
- public abstract interface ImeKeyMonitor {
- method public abstract void setImeKeyListener(android.support.v17.leanback.widget.ImeKeyMonitor.ImeKeyListener);
- }
-
- public static abstract interface ImeKeyMonitor.ImeKeyListener {
- method public abstract boolean onKeyPreIme(android.widget.EditText, int, android.view.KeyEvent);
- }
-
- public final class ItemAlignmentFacet {
- ctor public ItemAlignmentFacet();
- method public android.support.v17.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef[] getAlignmentDefs();
- method public boolean isMultiAlignment();
- method public void setAlignmentDefs(android.support.v17.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef[]);
- field public static final float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1.0f;
- }
-
- public static class ItemAlignmentFacet.ItemAlignmentDef {
- ctor public ItemAlignmentFacet.ItemAlignmentDef();
- method public final int getItemAlignmentFocusViewId();
- method public final int getItemAlignmentOffset();
- method public final float getItemAlignmentOffsetPercent();
- method public final int getItemAlignmentViewId();
- method public boolean isAlignedToTextViewBaseLine();
- method public final boolean isItemAlignmentOffsetWithPadding();
- method public final void setAlignedToTextViewBaseline(boolean);
- method public final void setItemAlignmentFocusViewId(int);
- method public final void setItemAlignmentOffset(int);
- method public final void setItemAlignmentOffsetPercent(float);
- method public final void setItemAlignmentOffsetWithPadding(boolean);
- method public final void setItemAlignmentViewId(int);
- }
-
- public class ItemBridgeAdapter extends android.support.v7.widget.RecyclerView.Adapter implements android.support.v17.leanback.widget.FacetProviderAdapter {
- ctor public ItemBridgeAdapter(android.support.v17.leanback.widget.ObjectAdapter, android.support.v17.leanback.widget.PresenterSelector);
- ctor public ItemBridgeAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- ctor public ItemBridgeAdapter();
- method public void clear();
- method public android.support.v17.leanback.widget.FacetProvider getFacetProvider(int);
- method public int getItemCount();
- method public java.util.ArrayList<android.support.v17.leanback.widget.Presenter> getPresenterMapper();
- method public android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper getWrapper();
- method protected void onAddPresenter(android.support.v17.leanback.widget.Presenter, int);
- method protected void onAttachedToWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method protected void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public final void onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int);
- method public final void onBindViewHolder(android.support.v7.widget.RecyclerView.ViewHolder, int, java.util.List);
- method protected void onCreate(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public final android.support.v7.widget.RecyclerView.ViewHolder onCreateViewHolder(android.view.ViewGroup, int);
- method protected void onDetachedFromWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method protected void onUnbind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public final void onViewAttachedToWindow(android.support.v7.widget.RecyclerView.ViewHolder);
- method public final void onViewDetachedFromWindow(android.support.v7.widget.RecyclerView.ViewHolder);
- method public final void onViewRecycled(android.support.v7.widget.RecyclerView.ViewHolder);
- method public void setAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public void setAdapterListener(android.support.v17.leanback.widget.ItemBridgeAdapter.AdapterListener);
- method public void setPresenter(android.support.v17.leanback.widget.PresenterSelector);
- method public void setPresenterMapper(java.util.ArrayList<android.support.v17.leanback.widget.Presenter>);
- method public void setWrapper(android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper);
- }
-
- public static class ItemBridgeAdapter.AdapterListener {
- ctor public ItemBridgeAdapter.AdapterListener();
- method public void onAddPresenter(android.support.v17.leanback.widget.Presenter, int);
- method public void onAttachedToWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public void onBind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder, java.util.List);
- method public void onCreate(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public void onDetachedFromWindow(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- method public void onUnbind(android.support.v17.leanback.widget.ItemBridgeAdapter.ViewHolder);
- }
-
- public class ItemBridgeAdapter.ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
- method public final java.lang.Object getExtraObject();
- method public java.lang.Object getFacet(java.lang.Class<?>);
- method public final java.lang.Object getItem();
- method public final android.support.v17.leanback.widget.Presenter getPresenter();
- method public final android.support.v17.leanback.widget.Presenter.ViewHolder getViewHolder();
- method public void setExtraObject(java.lang.Object);
- }
-
- public static abstract class ItemBridgeAdapter.Wrapper {
- ctor public ItemBridgeAdapter.Wrapper();
- method public abstract android.view.View createWrapper(android.view.View);
- method public abstract void wrap(android.view.View, android.view.View);
- }
-
- public class ItemBridgeAdapterShadowOverlayWrapper extends android.support.v17.leanback.widget.ItemBridgeAdapter.Wrapper {
- ctor public ItemBridgeAdapterShadowOverlayWrapper(android.support.v17.leanback.widget.ShadowOverlayHelper);
- method public android.view.View createWrapper(android.view.View);
- method public void wrap(android.view.View, android.view.View);
- }
-
- public class ListRow extends android.support.v17.leanback.widget.Row {
- ctor public ListRow(android.support.v17.leanback.widget.HeaderItem, android.support.v17.leanback.widget.ObjectAdapter);
- ctor public ListRow(long, android.support.v17.leanback.widget.HeaderItem, android.support.v17.leanback.widget.ObjectAdapter);
- ctor public ListRow(android.support.v17.leanback.widget.ObjectAdapter);
- method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
- method public java.lang.CharSequence getContentDescription();
- method public void setContentDescription(java.lang.CharSequence);
- }
-
- public final class ListRowHoverCardView extends android.widget.LinearLayout {
- ctor public ListRowHoverCardView(android.content.Context);
- ctor public ListRowHoverCardView(android.content.Context, android.util.AttributeSet);
- ctor public ListRowHoverCardView(android.content.Context, android.util.AttributeSet, int);
- method public final java.lang.CharSequence getDescription();
- method public final java.lang.CharSequence getTitle();
- method public final void setDescription(java.lang.CharSequence);
- method public final void setTitle(java.lang.CharSequence);
- }
-
- public class ListRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public ListRowPresenter();
- ctor public ListRowPresenter(int);
- ctor public ListRowPresenter(int, boolean);
- method protected void applySelectLevelToChild(android.support.v17.leanback.widget.ListRowPresenter.ViewHolder, android.view.View);
- method public final boolean areChildRoundedCornersEnabled();
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method protected android.support.v17.leanback.widget.ShadowOverlayHelper.Options createShadowOverlayOptions();
- method public final void enableChildRoundedCorners(boolean);
- method public int getExpandedRowHeight();
- method public final int getFocusZoomFactor();
- method public final android.support.v17.leanback.widget.PresenterSelector getHoverCardPresenterSelector();
- method public int getRecycledPoolSize(android.support.v17.leanback.widget.Presenter);
- method public int getRowHeight();
- method public final boolean getShadowEnabled();
- method public final deprecated int getZoomFactor();
- method public final boolean isFocusDimmerUsed();
- method public final boolean isKeepChildForeground();
- method public boolean isUsingDefaultListSelectEffect();
- method public final boolean isUsingDefaultSelectEffect();
- method public boolean isUsingDefaultShadow();
- method public boolean isUsingOutlineClipping(android.content.Context);
- method public boolean isUsingZOrder(android.content.Context);
- method public void setExpandedRowHeight(int);
- method public final void setHoverCardPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public final void setKeepChildForeground(boolean);
- method public void setNumRows(int);
- method public void setRecycledPoolSize(android.support.v17.leanback.widget.Presenter, int);
- method public void setRowHeight(int);
- method public final void setShadowEnabled(boolean);
- }
-
- public static class ListRowPresenter.SelectItemViewHolderTask extends android.support.v17.leanback.widget.Presenter.ViewHolderTask {
- ctor public ListRowPresenter.SelectItemViewHolderTask(int);
- method public int getItemPosition();
- method public android.support.v17.leanback.widget.Presenter.ViewHolderTask getItemTask();
- method public boolean isSmoothScroll();
- method public void setItemPosition(int);
- method public void setItemTask(android.support.v17.leanback.widget.Presenter.ViewHolderTask);
- method public void setSmoothScroll(boolean);
- }
-
- public static class ListRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public ListRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.HorizontalGridView, android.support.v17.leanback.widget.ListRowPresenter);
- method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
- method public final android.support.v17.leanback.widget.HorizontalGridView getGridView();
- method public android.support.v17.leanback.widget.Presenter.ViewHolder getItemViewHolder(int);
- method public final android.support.v17.leanback.widget.ListRowPresenter getListRowPresenter();
- method public int getSelectedPosition();
- }
-
- public final class ListRowView extends android.widget.LinearLayout {
- ctor public ListRowView(android.content.Context);
- ctor public ListRowView(android.content.Context, android.util.AttributeSet);
- ctor public ListRowView(android.content.Context, android.util.AttributeSet, int);
- method public android.support.v17.leanback.widget.HorizontalGridView getGridView();
- }
-
- public abstract interface MultiActionsProvider {
- method public abstract android.support.v17.leanback.widget.MultiActionsProvider.MultiAction[] getActions();
- }
-
- public static class MultiActionsProvider.MultiAction {
- ctor public MultiActionsProvider.MultiAction(long);
- method public android.graphics.drawable.Drawable getCurrentDrawable();
- method public android.graphics.drawable.Drawable[] getDrawables();
- method public long getId();
- method public int getIndex();
- method public void incrementIndex();
- method public void setDrawables(android.graphics.drawable.Drawable[]);
- method public void setIndex(int);
- }
-
- public abstract class ObjectAdapter {
- ctor public ObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
- ctor public ObjectAdapter(android.support.v17.leanback.widget.Presenter);
- ctor public ObjectAdapter();
- method public abstract java.lang.Object get(int);
- method public long getId(int);
- method public final android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
- method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
- method public final boolean hasStableIds();
- method public boolean isImmediateNotifySupported();
- method protected final void notifyChanged();
- method protected final void notifyItemMoved(int, int);
- method public final void notifyItemRangeChanged(int, int);
- method public final void notifyItemRangeChanged(int, int, java.lang.Object);
- method protected final void notifyItemRangeInserted(int, int);
- method protected final void notifyItemRangeRemoved(int, int);
- method protected void onHasStableIdsChanged();
- method protected void onPresenterSelectorChanged();
- method public final void registerObserver(android.support.v17.leanback.widget.ObjectAdapter.DataObserver);
- method public final void setHasStableIds(boolean);
- method public final void setPresenterSelector(android.support.v17.leanback.widget.PresenterSelector);
- method public abstract int size();
- method public final void unregisterAllObservers();
- method public final void unregisterObserver(android.support.v17.leanback.widget.ObjectAdapter.DataObserver);
- field public static final int NO_ID = -1; // 0xffffffff
- }
-
- public static abstract class ObjectAdapter.DataObserver {
- ctor public ObjectAdapter.DataObserver();
- method public void onChanged();
- method public void onItemMoved(int, int);
- method public void onItemRangeChanged(int, int);
- method public void onItemRangeChanged(int, int, java.lang.Object);
- method public void onItemRangeInserted(int, int);
- method public void onItemRangeRemoved(int, int);
- }
-
- public abstract interface OnActionClickedListener {
- method public abstract void onActionClicked(android.support.v17.leanback.widget.Action);
- }
-
- public abstract interface OnChildLaidOutListener {
- method public abstract void onChildLaidOut(android.view.ViewGroup, android.view.View, int, long);
- }
-
- public abstract deprecated interface OnChildSelectedListener {
- method public abstract void onChildSelected(android.view.ViewGroup, android.view.View, int, long);
- }
-
- public abstract class OnChildViewHolderSelectedListener {
- ctor public OnChildViewHolderSelectedListener();
- method public void onChildViewHolderSelected(android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.ViewHolder, int, int);
- method public void onChildViewHolderSelectedAndPositioned(android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.ViewHolder, int, int);
- }
-
- public abstract interface OnItemViewClickedListener implements android.support.v17.leanback.widget.BaseOnItemViewClickedListener {
- }
-
- public abstract interface OnItemViewSelectedListener implements android.support.v17.leanback.widget.BaseOnItemViewSelectedListener {
- }
-
- public class PageRow extends android.support.v17.leanback.widget.Row {
- ctor public PageRow(android.support.v17.leanback.widget.HeaderItem);
- method public final boolean isRenderedAsRowView();
- }
-
- public abstract class Parallax<PropertyT extends android.util.Property> {
- ctor public Parallax();
- method public android.support.v17.leanback.widget.ParallaxEffect addEffect(android.support.v17.leanback.widget.Parallax.PropertyMarkerValue...);
- method public final PropertyT addProperty(java.lang.String);
- method public abstract PropertyT createProperty(java.lang.String, int);
- method public java.util.List<android.support.v17.leanback.widget.ParallaxEffect> getEffects();
- method public abstract float getMaxValue();
- method public final java.util.List<PropertyT> getProperties();
- method public void removeAllEffects();
- method public void removeEffect(android.support.v17.leanback.widget.ParallaxEffect);
- method public void updateValues();
- }
-
- public static class Parallax.FloatProperty extends android.util.Property {
- ctor public Parallax.FloatProperty(java.lang.String, int);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue at(float, float);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atAbsolute(float);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atFraction(float);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMax();
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMin();
- method public final java.lang.Float get(android.support.v17.leanback.widget.Parallax);
- method public final int getIndex();
- method public final float getValue(android.support.v17.leanback.widget.Parallax);
- method public final void set(android.support.v17.leanback.widget.Parallax, java.lang.Float);
- method public final void setValue(android.support.v17.leanback.widget.Parallax, float);
- field public static final float UNKNOWN_AFTER = 3.4028235E38f;
- field public static final float UNKNOWN_BEFORE = -3.4028235E38f;
- }
-
- public static class Parallax.IntProperty extends android.util.Property {
- ctor public Parallax.IntProperty(java.lang.String, int);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue at(int, float);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atAbsolute(int);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atFraction(float);
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMax();
- method public final android.support.v17.leanback.widget.Parallax.PropertyMarkerValue atMin();
- method public final java.lang.Integer get(android.support.v17.leanback.widget.Parallax);
- method public final int getIndex();
- method public final int getValue(android.support.v17.leanback.widget.Parallax);
- method public final void set(android.support.v17.leanback.widget.Parallax, java.lang.Integer);
- method public final void setValue(android.support.v17.leanback.widget.Parallax, int);
- field public static final int UNKNOWN_AFTER = 2147483647; // 0x7fffffff
- field public static final int UNKNOWN_BEFORE = -2147483648; // 0x80000000
- }
-
- public static class Parallax.PropertyMarkerValue<PropertyT> {
- ctor public Parallax.PropertyMarkerValue(PropertyT);
- method public PropertyT getProperty();
- }
-
- public abstract class ParallaxEffect {
- method public final void addTarget(android.support.v17.leanback.widget.ParallaxTarget);
- method public final java.util.List<android.support.v17.leanback.widget.Parallax.PropertyMarkerValue> getPropertyRanges();
- method public final java.util.List<android.support.v17.leanback.widget.ParallaxTarget> getTargets();
- method public final void performMapping(android.support.v17.leanback.widget.Parallax);
- method public final void removeTarget(android.support.v17.leanback.widget.ParallaxTarget);
- method public final void setPropertyRanges(android.support.v17.leanback.widget.Parallax.PropertyMarkerValue...);
- method public final android.support.v17.leanback.widget.ParallaxEffect target(android.support.v17.leanback.widget.ParallaxTarget);
- method public final android.support.v17.leanback.widget.ParallaxEffect target(java.lang.Object, android.animation.PropertyValuesHolder);
- method public final <T, V extends java.lang.Number> android.support.v17.leanback.widget.ParallaxEffect target(T, android.util.Property<T, V>);
- }
-
- public abstract class ParallaxTarget {
- ctor public ParallaxTarget();
- method public void directUpdate(java.lang.Number);
- method public boolean isDirectMapping();
- method public void update(float);
- }
-
- public static final class ParallaxTarget.DirectPropertyTarget<T, V extends java.lang.Number> extends android.support.v17.leanback.widget.ParallaxTarget {
- ctor public ParallaxTarget.DirectPropertyTarget(java.lang.Object, android.util.Property<T, V>);
- }
-
- public static final class ParallaxTarget.PropertyValuesHolderTarget extends android.support.v17.leanback.widget.ParallaxTarget {
- ctor public ParallaxTarget.PropertyValuesHolderTarget(java.lang.Object, android.animation.PropertyValuesHolder);
- }
-
- public class PlaybackControlsRow extends android.support.v17.leanback.widget.Row {
- ctor public PlaybackControlsRow(java.lang.Object);
- ctor public PlaybackControlsRow();
- method public android.support.v17.leanback.widget.Action getActionForKeyCode(int);
- method public android.support.v17.leanback.widget.Action getActionForKeyCode(android.support.v17.leanback.widget.ObjectAdapter, int);
- method public long getBufferedPosition();
- method public deprecated int getBufferedProgress();
- method public deprecated long getBufferedProgressLong();
- method public long getCurrentPosition();
- method public deprecated int getCurrentTime();
- method public deprecated long getCurrentTimeLong();
- method public long getDuration();
- method public final android.graphics.drawable.Drawable getImageDrawable();
- method public final java.lang.Object getItem();
- method public final android.support.v17.leanback.widget.ObjectAdapter getPrimaryActionsAdapter();
- method public final android.support.v17.leanback.widget.ObjectAdapter getSecondaryActionsAdapter();
- method public deprecated int getTotalTime();
- method public deprecated long getTotalTimeLong();
- method public void setBufferedPosition(long);
- method public deprecated void setBufferedProgress(int);
- method public deprecated void setBufferedProgressLong(long);
- method public void setCurrentPosition(long);
- method public deprecated void setCurrentTime(int);
- method public deprecated void setCurrentTimeLong(long);
- method public void setDuration(long);
- method public final void setImageBitmap(android.content.Context, android.graphics.Bitmap);
- method public final void setImageDrawable(android.graphics.drawable.Drawable);
- method public void setOnPlaybackProgressChangedListener(android.support.v17.leanback.widget.PlaybackControlsRow.OnPlaybackProgressCallback);
- method public final void setPrimaryActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public final void setSecondaryActionsAdapter(android.support.v17.leanback.widget.ObjectAdapter);
- method public deprecated void setTotalTime(int);
- method public deprecated void setTotalTimeLong(long);
- }
-
- public static class PlaybackControlsRow.ClosedCaptioningAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.ClosedCaptioningAction(android.content.Context);
- ctor public PlaybackControlsRow.ClosedCaptioningAction(android.content.Context, int);
- field public static final int INDEX_OFF = 0; // 0x0
- field public static final int INDEX_ON = 1; // 0x1
- field public static deprecated int OFF;
- field public static deprecated int ON;
- }
-
- public static class PlaybackControlsRow.FastForwardAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.FastForwardAction(android.content.Context);
- ctor public PlaybackControlsRow.FastForwardAction(android.content.Context, int);
- }
-
- public static class PlaybackControlsRow.HighQualityAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.HighQualityAction(android.content.Context);
- ctor public PlaybackControlsRow.HighQualityAction(android.content.Context, int);
- field public static final int INDEX_OFF = 0; // 0x0
- field public static final int INDEX_ON = 1; // 0x1
- field public static deprecated int OFF;
- field public static deprecated int ON;
- }
-
- public static class PlaybackControlsRow.MoreActions extends android.support.v17.leanback.widget.Action {
- ctor public PlaybackControlsRow.MoreActions(android.content.Context);
- }
-
- public static abstract class PlaybackControlsRow.MultiAction extends android.support.v17.leanback.widget.Action {
- ctor public PlaybackControlsRow.MultiAction(int);
- method public int getActionCount();
- method public android.graphics.drawable.Drawable getDrawable(int);
- method public int getIndex();
- method public java.lang.String getLabel(int);
- method public java.lang.String getSecondaryLabel(int);
- method public void nextIndex();
- method public void setDrawables(android.graphics.drawable.Drawable[]);
- method public void setIndex(int);
- method public void setLabels(java.lang.String[]);
- method public void setSecondaryLabels(java.lang.String[]);
- }
-
- public static class PlaybackControlsRow.OnPlaybackProgressCallback {
- ctor public PlaybackControlsRow.OnPlaybackProgressCallback();
- method public void onBufferedPositionChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
- method public void onCurrentPositionChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
- method public void onDurationChanged(android.support.v17.leanback.widget.PlaybackControlsRow, long);
- }
-
- public static class PlaybackControlsRow.PictureInPictureAction extends android.support.v17.leanback.widget.Action {
- ctor public PlaybackControlsRow.PictureInPictureAction(android.content.Context);
- }
-
- public static class PlaybackControlsRow.PlayPauseAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.PlayPauseAction(android.content.Context);
- field public static final int INDEX_PAUSE = 1; // 0x1
- field public static final int INDEX_PLAY = 0; // 0x0
- field public static deprecated int PAUSE;
- field public static deprecated int PLAY;
- }
-
- public static class PlaybackControlsRow.RepeatAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.RepeatAction(android.content.Context);
- ctor public PlaybackControlsRow.RepeatAction(android.content.Context, int);
- ctor public PlaybackControlsRow.RepeatAction(android.content.Context, int, int);
- field public static deprecated int ALL;
- field public static final int INDEX_ALL = 1; // 0x1
- field public static final int INDEX_NONE = 0; // 0x0
- field public static final int INDEX_ONE = 2; // 0x2
- field public static deprecated int NONE;
- field public static deprecated int ONE;
- }
-
- public static class PlaybackControlsRow.RewindAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.RewindAction(android.content.Context);
- ctor public PlaybackControlsRow.RewindAction(android.content.Context, int);
- }
-
- public static class PlaybackControlsRow.ShuffleAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.ShuffleAction(android.content.Context);
- ctor public PlaybackControlsRow.ShuffleAction(android.content.Context, int);
- field public static final int INDEX_OFF = 0; // 0x0
- field public static final int INDEX_ON = 1; // 0x1
- field public static deprecated int OFF;
- field public static deprecated int ON;
- }
-
- public static class PlaybackControlsRow.SkipNextAction extends android.support.v17.leanback.widget.Action {
- ctor public PlaybackControlsRow.SkipNextAction(android.content.Context);
- }
-
- public static class PlaybackControlsRow.SkipPreviousAction extends android.support.v17.leanback.widget.Action {
- ctor public PlaybackControlsRow.SkipPreviousAction(android.content.Context);
- }
-
- public static abstract class PlaybackControlsRow.ThumbsAction extends android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction {
- ctor public PlaybackControlsRow.ThumbsAction(int, android.content.Context, int, int);
- field public static final int INDEX_OUTLINE = 1; // 0x1
- field public static final int INDEX_SOLID = 0; // 0x0
- field public static deprecated int OUTLINE;
- field public static deprecated int SOLID;
- }
-
- public static class PlaybackControlsRow.ThumbsDownAction extends android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsAction {
- ctor public PlaybackControlsRow.ThumbsDownAction(android.content.Context);
- }
-
- public static class PlaybackControlsRow.ThumbsUpAction extends android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsAction {
- ctor public PlaybackControlsRow.ThumbsUpAction(android.content.Context);
- }
-
- public class PlaybackControlsRowPresenter extends android.support.v17.leanback.widget.PlaybackRowPresenter {
- ctor public PlaybackControlsRowPresenter(android.support.v17.leanback.widget.Presenter);
- ctor public PlaybackControlsRowPresenter();
- method public boolean areSecondaryActionsHidden();
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method public int getBackgroundColor();
- method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
- method public int getProgressColor();
- method public void setBackgroundColor(int);
- method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
- method public void setProgressColor(int);
- method public void setSecondaryActionsHidden(boolean);
- method public void showBottomSpace(android.support.v17.leanback.widget.PlaybackControlsRowPresenter.ViewHolder, boolean);
- method public void showPrimaryActions(android.support.v17.leanback.widget.PlaybackControlsRowPresenter.ViewHolder);
- }
-
- public class PlaybackControlsRowPresenter.ViewHolder extends android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder {
- field public final android.support.v17.leanback.widget.Presenter.ViewHolder mDescriptionViewHolder;
- }
-
- public abstract class PlaybackRowPresenter extends android.support.v17.leanback.widget.RowPresenter {
- ctor public PlaybackRowPresenter();
- method public void onReappear(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- }
-
- public static class PlaybackRowPresenter.ViewHolder extends android.support.v17.leanback.widget.RowPresenter.ViewHolder {
- ctor public PlaybackRowPresenter.ViewHolder(android.view.View);
- }
-
- public class PlaybackSeekDataProvider {
- ctor public PlaybackSeekDataProvider();
- method public long[] getSeekPositions();
- method public void getThumbnail(int, android.support.v17.leanback.widget.PlaybackSeekDataProvider.ResultCallback);
- method public void reset();
- }
-
- public static class PlaybackSeekDataProvider.ResultCallback {
- ctor public PlaybackSeekDataProvider.ResultCallback();
- method public void onThumbnailLoaded(android.graphics.Bitmap, int);
- }
-
- public abstract interface PlaybackSeekUi {
- method public abstract void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- }
-
- public static class PlaybackSeekUi.Client {
- ctor public PlaybackSeekUi.Client();
- method public android.support.v17.leanback.widget.PlaybackSeekDataProvider getPlaybackSeekDataProvider();
- method public boolean isSeekEnabled();
- method public void onSeekFinished(boolean);
- method public void onSeekPositionChanged(long);
- method public void onSeekStarted();
- }
-
- public class PlaybackTransportRowPresenter extends android.support.v17.leanback.widget.PlaybackRowPresenter {
- ctor public PlaybackTransportRowPresenter();
- method protected android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method public float getDefaultSeekIncrement();
- method public android.support.v17.leanback.widget.OnActionClickedListener getOnActionClickedListener();
- method public int getProgressColor();
- method protected void onProgressBarClicked(android.support.v17.leanback.widget.PlaybackTransportRowPresenter.ViewHolder);
- method public void setDefaultSeekIncrement(float);
- method public void setDescriptionPresenter(android.support.v17.leanback.widget.Presenter);
- method public void setOnActionClickedListener(android.support.v17.leanback.widget.OnActionClickedListener);
- method public void setProgressColor(int);
- }
-
- public class PlaybackTransportRowPresenter.ViewHolder extends android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder implements android.support.v17.leanback.widget.PlaybackSeekUi {
- ctor public PlaybackTransportRowPresenter.ViewHolder(android.view.View, android.support.v17.leanback.widget.Presenter);
- method public final android.widget.TextView getCurrentPositionView();
- method public final android.support.v17.leanback.widget.Presenter.ViewHolder getDescriptionViewHolder();
- method public final android.widget.TextView getDurationView();
- method protected void onSetCurrentPositionLabel(long);
- method protected void onSetDurationLabel(long);
- method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
- }
-
- public abstract class Presenter implements android.support.v17.leanback.widget.FacetProvider {
- ctor public Presenter();
- method protected static void cancelAnimationsRecursive(android.view.View);
- method public final java.lang.Object getFacet(java.lang.Class<?>);
- method public abstract void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object, java.util.List<java.lang.Object>);
- method public abstract android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public abstract void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void onViewAttachedToWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void onViewDetachedFromWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public final void setFacet(java.lang.Class<?>, java.lang.Object);
- method public void setOnClickListener(android.support.v17.leanback.widget.Presenter.ViewHolder, android.view.View.OnClickListener);
- }
-
- public static class Presenter.ViewHolder implements android.support.v17.leanback.widget.FacetProvider {
- ctor public Presenter.ViewHolder(android.view.View);
- method public final java.lang.Object getFacet(java.lang.Class<?>);
- method public final void setFacet(java.lang.Class<?>, java.lang.Object);
- field public final android.view.View view;
- }
-
- public static abstract class Presenter.ViewHolderTask {
- ctor public Presenter.ViewHolderTask();
- method public void run(android.support.v17.leanback.widget.Presenter.ViewHolder);
- }
-
- public abstract class PresenterSelector {
- ctor public PresenterSelector();
- method public abstract android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
- method public android.support.v17.leanback.widget.Presenter[] getPresenters();
- }
-
- public abstract class PresenterSwitcher {
- ctor public PresenterSwitcher();
- method public void clear();
- method public final android.view.ViewGroup getParentViewGroup();
- method public void init(android.view.ViewGroup, android.support.v17.leanback.widget.PresenterSelector);
- method protected abstract void insertView(android.view.View);
- method protected void onViewSelected(android.view.View);
- method public void select(java.lang.Object);
- method protected void showView(android.view.View, boolean);
- method public void unselect();
- }
-
- public class RecyclerViewParallax extends android.support.v17.leanback.widget.Parallax {
- ctor public RecyclerViewParallax();
- method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty createProperty(java.lang.String, int);
- method public float getMaxValue();
- method public android.support.v7.widget.RecyclerView getRecyclerView();
- method public void setRecyclerView(android.support.v7.widget.RecyclerView);
- }
-
- public static final class RecyclerViewParallax.ChildPositionProperty extends android.support.v17.leanback.widget.Parallax.IntProperty {
- method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty adapterPosition(int);
- method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty fraction(float);
- method public int getAdapterPosition();
- method public float getFraction();
- method public int getOffset();
- method public int getViewId();
- method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty offset(int);
- method public android.support.v17.leanback.widget.RecyclerViewParallax.ChildPositionProperty viewId(int);
- }
-
- public class Row {
- ctor public Row(long, android.support.v17.leanback.widget.HeaderItem);
- ctor public Row(android.support.v17.leanback.widget.HeaderItem);
- ctor public Row();
- method public final android.support.v17.leanback.widget.HeaderItem getHeaderItem();
- method public final long getId();
- method public boolean isRenderedAsRowView();
- method public final void setHeaderItem(android.support.v17.leanback.widget.HeaderItem);
- method public final void setId(long);
- }
-
- public class RowHeaderPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public RowHeaderPresenter();
- method protected static float getFontDescent(android.widget.TextView, android.graphics.Paint);
- method public int getSpaceUnderBaseline(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder);
- method public boolean isNullItemVisibilityGone();
- method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method protected void onSelectLevelChanged(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder);
- method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void setNullItemVisibilityGone(boolean);
- method public final void setSelectLevel(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, float);
- }
-
- public static class RowHeaderPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
- ctor public RowHeaderPresenter.ViewHolder(android.view.View);
- method public final float getSelectLevel();
- }
-
- public final class RowHeaderView extends android.widget.TextView {
- ctor public RowHeaderView(android.content.Context);
- ctor public RowHeaderView(android.content.Context, android.util.AttributeSet);
- ctor public RowHeaderView(android.content.Context, android.util.AttributeSet, int);
- }
-
- public abstract class RowPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public RowPresenter();
- method protected abstract android.support.v17.leanback.widget.RowPresenter.ViewHolder createRowViewHolder(android.view.ViewGroup);
- method protected void dispatchItemSelectedListener(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
- method public void freeze(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
- method public final android.support.v17.leanback.widget.RowHeaderPresenter getHeaderPresenter();
- method public final android.support.v17.leanback.widget.RowPresenter.ViewHolder getRowViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public final boolean getSelectEffectEnabled();
- method public final float getSelectLevel(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public final int getSyncActivatePolicy();
- method protected void initializeRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- method protected boolean isClippingChildren();
- method public boolean isUsingDefaultSelectEffect();
- method protected void onBindRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder, java.lang.Object);
- method public final void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public final android.support.v17.leanback.widget.Presenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method protected void onRowViewAttachedToWindow(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- method protected void onRowViewDetachedFromWindow(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- method protected void onRowViewExpanded(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
- method protected void onRowViewSelected(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
- method protected void onSelectLevelChanged(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- method protected void onUnbindRowViewHolder(android.support.v17.leanback.widget.RowPresenter.ViewHolder);
- method public final void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public final void onViewAttachedToWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public final void onViewDetachedFromWindow(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void setEntranceTransitionState(android.support.v17.leanback.widget.RowPresenter.ViewHolder, boolean);
- method public final void setHeaderPresenter(android.support.v17.leanback.widget.RowHeaderPresenter);
- method public final void setRowViewExpanded(android.support.v17.leanback.widget.Presenter.ViewHolder, boolean);
- method public final void setRowViewSelected(android.support.v17.leanback.widget.Presenter.ViewHolder, boolean);
- method public final void setSelectEffectEnabled(boolean);
- method public final void setSelectLevel(android.support.v17.leanback.widget.Presenter.ViewHolder, float);
- method public final void setSyncActivatePolicy(int);
- field public static final int SYNC_ACTIVATED_CUSTOM = 0; // 0x0
- field public static final int SYNC_ACTIVATED_TO_EXPANDED = 1; // 0x1
- field public static final int SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED = 3; // 0x3
- field public static final int SYNC_ACTIVATED_TO_SELECTED = 2; // 0x2
- }
-
- public static class RowPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
- ctor public RowPresenter.ViewHolder(android.view.View);
- method public final android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder getHeaderViewHolder();
- method public final android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
- method public final android.support.v17.leanback.widget.BaseOnItemViewSelectedListener getOnItemViewSelectedListener();
- method public android.view.View.OnKeyListener getOnKeyListener();
- method public final android.support.v17.leanback.widget.Row getRow();
- method public final java.lang.Object getRowObject();
- method public final float getSelectLevel();
- method public java.lang.Object getSelectedItem();
- method public android.support.v17.leanback.widget.Presenter.ViewHolder getSelectedItemViewHolder();
- method public final boolean isExpanded();
- method public final boolean isSelected();
- method public final void setActivated(boolean);
- method public final void setOnItemViewClickedListener(android.support.v17.leanback.widget.BaseOnItemViewClickedListener);
- method public final void setOnItemViewSelectedListener(android.support.v17.leanback.widget.BaseOnItemViewSelectedListener);
- method public void setOnKeyListener(android.view.View.OnKeyListener);
- method public final void syncActivatedStatus(android.view.View);
- field protected final android.support.v17.leanback.graphics.ColorOverlayDimmer mColorDimmer;
- }
-
- public class SearchBar extends android.widget.RelativeLayout {
- ctor public SearchBar(android.content.Context);
- ctor public SearchBar(android.content.Context, android.util.AttributeSet);
- ctor public SearchBar(android.content.Context, android.util.AttributeSet, int);
- method public void displayCompletions(java.util.List<java.lang.String>);
- method public void displayCompletions(android.view.inputmethod.CompletionInfo[]);
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public java.lang.CharSequence getHint();
- method public java.lang.String getTitle();
- method public boolean isRecognizing();
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setPermissionListener(android.support.v17.leanback.widget.SearchBar.SearchBarPermissionListener);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchAffordanceColorsInListening(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSearchBarListener(android.support.v17.leanback.widget.SearchBar.SearchBarListener);
- method public void setSearchQuery(java.lang.String);
- method public deprecated void setSpeechRecognitionCallback(android.support.v17.leanback.widget.SpeechRecognitionCallback);
- method public void setSpeechRecognizer(android.speech.SpeechRecognizer);
- method public void setTitle(java.lang.String);
- method public void startRecognition();
- method public void stopRecognition();
- }
-
- public static abstract interface SearchBar.SearchBarListener {
- method public abstract void onKeyboardDismiss(java.lang.String);
- method public abstract void onSearchQueryChange(java.lang.String);
- method public abstract void onSearchQuerySubmit(java.lang.String);
- }
-
- public static abstract interface SearchBar.SearchBarPermissionListener {
- method public abstract void requestAudioPermission();
- }
-
- public class SearchEditText extends android.support.v17.leanback.widget.StreamingTextView {
- ctor public SearchEditText(android.content.Context);
- ctor public SearchEditText(android.content.Context, android.util.AttributeSet);
- ctor public SearchEditText(android.content.Context, android.util.AttributeSet, int);
- method public void setOnKeyboardDismissListener(android.support.v17.leanback.widget.SearchEditText.OnKeyboardDismissListener);
- }
-
- public static abstract interface SearchEditText.OnKeyboardDismissListener {
- method public abstract void onKeyboardDismiss();
- }
-
- public class SearchOrbView extends android.widget.FrameLayout implements android.view.View.OnClickListener {
- ctor public SearchOrbView(android.content.Context);
- ctor public SearchOrbView(android.content.Context, android.util.AttributeSet);
- ctor public SearchOrbView(android.content.Context, android.util.AttributeSet, int);
- method public void enableOrbColorAnimation(boolean);
- method public int getOrbColor();
- method public android.support.v17.leanback.widget.SearchOrbView.Colors getOrbColors();
- method public android.graphics.drawable.Drawable getOrbIcon();
- method public void onClick(android.view.View);
- method public void setOnOrbClickedListener(android.view.View.OnClickListener);
- method public void setOrbColor(int);
- method public deprecated void setOrbColor(int, int);
- method public void setOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setOrbIcon(android.graphics.drawable.Drawable);
- }
-
- public static class SearchOrbView.Colors {
- ctor public SearchOrbView.Colors(int);
- ctor public SearchOrbView.Colors(int, int);
- ctor public SearchOrbView.Colors(int, int, int);
- method public static int getBrightColor(int);
- field public int brightColor;
- field public int color;
- field public int iconColor;
- }
-
- public class SectionRow extends android.support.v17.leanback.widget.Row {
- ctor public SectionRow(android.support.v17.leanback.widget.HeaderItem);
- ctor public SectionRow(long, java.lang.String);
- ctor public SectionRow(java.lang.String);
- method public final boolean isRenderedAsRowView();
- }
-
- public class ShadowOverlayContainer extends android.widget.FrameLayout {
- ctor public ShadowOverlayContainer(android.content.Context);
- ctor public ShadowOverlayContainer(android.content.Context, android.util.AttributeSet);
- ctor public ShadowOverlayContainer(android.content.Context, android.util.AttributeSet, int);
- method public int getShadowType();
- method public android.view.View getWrappedView();
- method public deprecated void initialize(boolean, boolean);
- method public deprecated void initialize(boolean, boolean, boolean);
- method public static void prepareParentForShadow(android.view.ViewGroup);
- method public void setOverlayColor(int);
- method public void setShadowFocusLevel(float);
- method public static boolean supportsDynamicShadow();
- method public static boolean supportsShadow();
- method public void useDynamicShadow();
- method public void useDynamicShadow(float, float);
- method public void useStaticShadow();
- method public void wrap(android.view.View);
- field public static final int SHADOW_DYNAMIC = 3; // 0x3
- field public static final int SHADOW_NONE = 1; // 0x1
- field public static final int SHADOW_STATIC = 2; // 0x2
- }
-
- public final class ShadowOverlayHelper {
- method public android.support.v17.leanback.widget.ShadowOverlayContainer createShadowOverlayContainer(android.content.Context);
- method public int getShadowType();
- method public boolean needsOverlay();
- method public boolean needsRoundedCorner();
- method public boolean needsWrapper();
- method public void onViewCreated(android.view.View);
- method public void prepareParentForShadow(android.view.ViewGroup);
- method public static void setNoneWrapperOverlayColor(android.view.View, int);
- method public static void setNoneWrapperShadowFocusLevel(android.view.View, float);
- method public void setOverlayColor(android.view.View, int);
- method public void setShadowFocusLevel(android.view.View, float);
- method public static boolean supportsDynamicShadow();
- method public static boolean supportsForeground();
- method public static boolean supportsRoundedCorner();
- method public static boolean supportsShadow();
- field public static final int SHADOW_DYNAMIC = 3; // 0x3
- field public static final int SHADOW_NONE = 1; // 0x1
- field public static final int SHADOW_STATIC = 2; // 0x2
- }
-
- public static final class ShadowOverlayHelper.Builder {
- ctor public ShadowOverlayHelper.Builder();
- method public android.support.v17.leanback.widget.ShadowOverlayHelper build(android.content.Context);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder keepForegroundDrawable(boolean);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsOverlay(boolean);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsRoundedCorner(boolean);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder needsShadow(boolean);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder options(android.support.v17.leanback.widget.ShadowOverlayHelper.Options);
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Builder preferZOrder(boolean);
- }
-
- public static final class ShadowOverlayHelper.Options {
- ctor public ShadowOverlayHelper.Options();
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Options dynamicShadowZ(float, float);
- method public final float getDynamicShadowFocusedZ();
- method public final float getDynamicShadowUnfocusedZ();
- method public final int getRoundedCornerRadius();
- method public android.support.v17.leanback.widget.ShadowOverlayHelper.Options roundedCornerRadius(int);
- field public static final android.support.v17.leanback.widget.ShadowOverlayHelper.Options DEFAULT;
- }
-
- public final class SinglePresenterSelector extends android.support.v17.leanback.widget.PresenterSelector {
- ctor public SinglePresenterSelector(android.support.v17.leanback.widget.Presenter);
- method public android.support.v17.leanback.widget.Presenter getPresenter(java.lang.Object);
- }
-
- public class SparseArrayObjectAdapter extends android.support.v17.leanback.widget.ObjectAdapter {
- ctor public SparseArrayObjectAdapter(android.support.v17.leanback.widget.PresenterSelector);
- ctor public SparseArrayObjectAdapter(android.support.v17.leanback.widget.Presenter);
- ctor public SparseArrayObjectAdapter();
- method public void clear(int);
- method public void clear();
- method public java.lang.Object get(int);
- method public int indexOf(java.lang.Object);
- method public int indexOf(int);
- method public java.lang.Object lookup(int);
- method public void notifyArrayItemRangeChanged(int, int);
- method public void set(int, java.lang.Object);
- method public int size();
- }
-
- public class SpeechOrbView extends android.support.v17.leanback.widget.SearchOrbView {
- ctor public SpeechOrbView(android.content.Context);
- ctor public SpeechOrbView(android.content.Context, android.util.AttributeSet);
- ctor public SpeechOrbView(android.content.Context, android.util.AttributeSet, int);
- method public void setListeningOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setNotListeningOrbColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setSoundLevel(int);
- method public void showListening();
- method public void showNotListening();
- }
-
- public abstract deprecated interface SpeechRecognitionCallback {
- method public abstract void recognizeSpeech();
- }
-
- class StreamingTextView extends android.widget.EditText {
- ctor public StreamingTextView(android.content.Context, android.util.AttributeSet);
- ctor public StreamingTextView(android.content.Context, android.util.AttributeSet, int);
- method public static boolean isLayoutRtl(android.view.View);
- method public void reset();
- method public void setFinalRecognizedText(java.lang.CharSequence);
- method public void updateRecognizedText(java.lang.String, java.lang.String);
- method public void updateRecognizedText(java.lang.String, java.util.List<java.lang.Float>);
- }
-
- public class TitleHelper {
- ctor public TitleHelper(android.view.ViewGroup, android.view.View);
- method public android.support.v17.leanback.widget.BrowseFrameLayout.OnFocusSearchListener getOnFocusSearchListener();
- method public android.view.ViewGroup getSceneRoot();
- method public android.view.View getTitleView();
- method public void showTitle(boolean);
- }
-
- public class TitleView extends android.widget.FrameLayout implements android.support.v17.leanback.widget.TitleViewAdapter.Provider {
- ctor public TitleView(android.content.Context);
- ctor public TitleView(android.content.Context, android.util.AttributeSet);
- ctor public TitleView(android.content.Context, android.util.AttributeSet, int);
- method public void enableAnimation(boolean);
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
- method public android.view.View getSearchAffordanceView();
- method public java.lang.CharSequence getTitle();
- method public android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnSearchClickedListener(android.view.View.OnClickListener);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setTitle(java.lang.CharSequence);
- method public void updateComponentsVisibility(int);
- }
-
- public abstract class TitleViewAdapter {
- ctor public TitleViewAdapter();
- method public android.graphics.drawable.Drawable getBadgeDrawable();
- method public android.support.v17.leanback.widget.SearchOrbView.Colors getSearchAffordanceColors();
- method public abstract android.view.View getSearchAffordanceView();
- method public java.lang.CharSequence getTitle();
- method public void setAnimationEnabled(boolean);
- method public void setBadgeDrawable(android.graphics.drawable.Drawable);
- method public void setOnSearchClickedListener(android.view.View.OnClickListener);
- method public void setSearchAffordanceColors(android.support.v17.leanback.widget.SearchOrbView.Colors);
- method public void setTitle(java.lang.CharSequence);
- method public void updateComponentsVisibility(int);
- field public static final int BRANDING_VIEW_VISIBLE = 2; // 0x2
- field public static final int FULL_VIEW_VISIBLE = 6; // 0x6
- field public static final int SEARCH_VIEW_VISIBLE = 4; // 0x4
- }
-
- public static abstract interface TitleViewAdapter.Provider {
- method public abstract android.support.v17.leanback.widget.TitleViewAdapter getTitleViewAdapter();
- }
-
- public class VerticalGridPresenter extends android.support.v17.leanback.widget.Presenter {
- ctor public VerticalGridPresenter();
- ctor public VerticalGridPresenter(int);
- ctor public VerticalGridPresenter(int, boolean);
- method public final boolean areChildRoundedCornersEnabled();
- method protected android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder createGridViewHolder(android.view.ViewGroup);
- method protected android.support.v17.leanback.widget.ShadowOverlayHelper.Options createShadowOverlayOptions();
- method public final void enableChildRoundedCorners(boolean);
- method public final int getFocusZoomFactor();
- method public final boolean getKeepChildForeground();
- method public int getNumberOfColumns();
- method public final android.support.v17.leanback.widget.OnItemViewClickedListener getOnItemViewClickedListener();
- method public final android.support.v17.leanback.widget.OnItemViewSelectedListener getOnItemViewSelectedListener();
- method public final boolean getShadowEnabled();
- method protected void initializeGridViewHolder(android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder);
- method public final boolean isFocusDimmerUsed();
- method public boolean isUsingDefaultShadow();
- method public boolean isUsingZOrder(android.content.Context);
- method public void onBindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder, java.lang.Object);
- method public final android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder onCreateViewHolder(android.view.ViewGroup);
- method public void onUnbindViewHolder(android.support.v17.leanback.widget.Presenter.ViewHolder);
- method public void setEntranceTransitionState(android.support.v17.leanback.widget.VerticalGridPresenter.ViewHolder, boolean);
- method public final void setKeepChildForeground(boolean);
- method public void setNumberOfColumns(int);
- method public final void setOnItemViewClickedListener(android.support.v17.leanback.widget.OnItemViewClickedListener);
- method public final void setOnItemViewSelectedListener(android.support.v17.leanback.widget.OnItemViewSelectedListener);
- method public final void setShadowEnabled(boolean);
- }
-
- public static class VerticalGridPresenter.ViewHolder extends android.support.v17.leanback.widget.Presenter.ViewHolder {
- ctor public VerticalGridPresenter.ViewHolder(android.support.v17.leanback.widget.VerticalGridView);
- method public android.support.v17.leanback.widget.VerticalGridView getGridView();
- }
-
- public class VerticalGridView extends android.support.v17.leanback.widget.BaseGridView {
- ctor public VerticalGridView(android.content.Context);
- ctor public VerticalGridView(android.content.Context, android.util.AttributeSet);
- ctor public VerticalGridView(android.content.Context, android.util.AttributeSet, int);
- method protected void initAttributes(android.content.Context, android.util.AttributeSet);
- method public void setColumnWidth(int);
- method public void setNumColumns(int);
- }
-
- public abstract interface ViewHolderTask {
- method public abstract void run(android.support.v7.widget.RecyclerView.ViewHolder);
- }
-
-}
-
-package android.support.v17.leanback.widget.picker {
-
- public class Picker extends android.widget.FrameLayout {
- ctor public Picker(android.content.Context, android.util.AttributeSet, int);
- method public void addOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
- method public float getActivatedVisibleItemCount();
- method public android.support.v17.leanback.widget.picker.PickerColumn getColumnAt(int);
- method public int getColumnsCount();
- method protected int getPickerItemHeightPixels();
- method public final int getPickerItemLayoutId();
- method public final int getPickerItemTextViewId();
- method public int getSelectedColumn();
- method public final deprecated java.lang.CharSequence getSeparator();
- method public final java.util.List<java.lang.CharSequence> getSeparators();
- method public float getVisibleItemCount();
- method public void onColumnValueChanged(int, int);
- method public void removeOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
- method public void setActivatedVisibleItemCount(float);
- method public void setColumnAt(int, android.support.v17.leanback.widget.picker.PickerColumn);
- method public void setColumnValue(int, int, boolean);
- method public void setColumns(java.util.List<android.support.v17.leanback.widget.picker.PickerColumn>);
- method public final void setPickerItemTextViewId(int);
- method public void setSelectedColumn(int);
- method public final void setSeparator(java.lang.CharSequence);
- method public final void setSeparators(java.util.List<java.lang.CharSequence>);
- method public void setVisibleItemCount(float);
- }
-
- public static abstract interface Picker.PickerValueListener {
- method public abstract void onValueChanged(android.support.v17.leanback.widget.picker.Picker, int);
- }
-
- public class PickerColumn {
- ctor public PickerColumn();
- method public int getCount();
- method public int getCurrentValue();
- method public java.lang.CharSequence getLabelFor(int);
- method public java.lang.String getLabelFormat();
- method public int getMaxValue();
- method public int getMinValue();
- method public java.lang.CharSequence[] getStaticLabels();
- method public void setCurrentValue(int);
- method public void setLabelFormat(java.lang.String);
- method public void setMaxValue(int);
- method public void setMinValue(int);
- method public void setStaticLabels(java.lang.CharSequence[]);
- }
-
- public class TimePicker extends android.support.v17.leanback.widget.picker.Picker {
- ctor public TimePicker(android.content.Context, android.util.AttributeSet);
- ctor public TimePicker(android.content.Context, android.util.AttributeSet, int);
- method public int getHour();
- method public int getMinute();
- method public boolean is24Hour();
- method public boolean isPm();
- method public void setHour(int);
- method public void setIs24Hour(boolean);
- method public void setMinute(int);
- }
-
-}
-
diff --git a/v17/leanback/generatef.py b/v17/leanback/generatef.py
deleted file mode 100755
index 04e303a..0000000
--- a/v17/leanback/generatef.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/python
-
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import sys
-import re
-
-print "Generate framework fragment related code for leanback"
-
-cls = ['Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
- 'Playback', 'Rows', 'Search', 'VerticalGrid', 'Branded',
- 'GuidedStep', 'Onboarding', 'Video']
-
-for w in cls:
- print "copy {}SupportFragment to {}Fragment".format(w, w)
-
- file = open('src/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'r')
- content = "// CHECKSTYLE:OFF Generated code\n"
- content = content + "/* This file is auto-generated from {}SupportFragment.java. DO NOT MODIFY. */\n\n".format(w)
-
- for line in file:
- line = line.replace('IS_FRAMEWORK_FRAGMENT = false', 'IS_FRAMEWORK_FRAGMENT = true');
- for w2 in cls:
- line = line.replace('{}SupportFragment'.format(w2), '{}Fragment'.format(w2))
- line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
- line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
- line = line.replace('activity.getSupportFragmentManager()', 'activity.getFragmentManager()')
- line = line.replace('FragmentActivity activity', 'Activity activity')
- line = line.replace('(FragmentActivity', '(Activity')
- # replace getContext() with FragmentUtil.getContext(XXXFragment.this), but dont match the case "view.getContext()"
- line = re.sub(r'([^\.])getContext\(\)', r'\1FragmentUtil.getContext({}Fragment.this)'.format(w), line);
- content = content + line
- file.close()
- # add deprecated tag to fragment class and inner classes/interfaces
- # content = re.sub(r'\*\/\n(@.*\n|)(public |abstract public |abstract |)class', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n@Deprecated\n\\1\\2class', content)
- # content = re.sub(r'\*\/\n public (static class|interface|final static class|abstract static class)', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n @Deprecated\n public \\1', content)
- outfile = open('src/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'w')
- outfile.write(content)
- outfile.close()
-
-
-
-print "copy VideoSupportFragmentGlueHost to VideoFragmentGlueHost"
-file = open('src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java', 'r')
-content = "// CHECKSTYLE:OFF Generated code\n"
-content = content + "/* This file is auto-generated from VideoSupportFragmentGlueHost.java. DO NOT MODIFY. */\n\n"
-for line in file:
- line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
- line = line.replace('VideoSupportFragment', 'VideoFragment')
- line = line.replace('PlaybackSupportFragment', 'PlaybackFragment')
- content = content + line
-file.close()
-# add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link VideoSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
-outfile = open('src/android/support/v17/leanback/app/VideoFragmentGlueHost.java', 'w')
-outfile.write(content)
-outfile.close()
-
-
-
-print "copy PlaybackSupportFragmentGlueHost to PlaybackFragmentGlueHost"
-file = open('src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java', 'r')
-content = "// CHECKSTYLE:OFF Generated code\n"
-content = content + "/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java. DO NOT MODIFY. */\n\n"
-for line in file:
- line = line.replace('VideoSupportFragment', 'VideoFragment')
- line = line.replace('PlaybackSupportFragment', 'PlaybackFragment')
- line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
- content = content + line
-file.close()
-# add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link PlaybackSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
-outfile = open('src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java', 'w')
-outfile.write(content)
-outfile.close()
-
-
-
-print "copy DetailsSupportFragmentBackgroundController to DetailsFragmentBackgroundController"
-file = open('src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java', 'r')
-content = "// CHECKSTYLE:OFF Generated code\n"
-content = content + "/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java. DO NOT MODIFY. */\n\n"
-for line in file:
- line = line.replace('VideoSupportFragment', 'VideoFragment')
- line = line.replace('DetailsSupportFragment', 'DetailsFragment')
- line = line.replace('RowsSupportFragment', 'RowsFragment')
- line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
- line = line.replace('mFragment.getContext()', 'FragmentUtil.getContext(mFragment)')
- content = content + line
-file.close()
-# add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link DetailsSupportFragmentBackgroundController}\n */\n@Deprecated\npublic class', content)
-outfile = open('src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java', 'w')
-outfile.write(content)
-outfile.close()
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
deleted file mode 100644
index bdb213f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
+++ /dev/null
@@ -1,321 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BaseSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.annotation.SuppressLint;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.transition.TransitionListener;
-import android.support.v17.leanback.util.StateMachine;
-import android.support.v17.leanback.util.StateMachine.Condition;
-import android.support.v17.leanback.util.StateMachine.Event;
-import android.support.v17.leanback.util.StateMachine.State;
-import android.view.View;
-import android.view.ViewTreeObserver;
-
-/**
- * Base class for leanback Fragments. This class is not intended to be subclassed by apps.
- */
-@SuppressWarnings("FragmentNotInstantiable")
-public class BaseFragment extends BrandedFragment {
-
- /**
- * The start state for all
- */
- final State STATE_START = new State("START", true, false);
-
- /**
- * Initial State for ENTRNACE transition.
- */
- final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
-
- /**
- * prepareEntranceTransition is just called, but view not ready yet. We can enable the
- * busy spinner.
- */
- final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
- @Override
- public void run() {
- mProgressBarManager.show();
- }
- };
-
- /**
- * prepareEntranceTransition is called and main content view to slide in was created, so we can
- * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
- * in this State, the process is very different in subclass, e.g. BrowseFragment hide header
- * views and hide main fragment view in two steps.
- */
- final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
- "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
- @Override
- public void run() {
- onEntranceTransitionPrepare();
- }
- };
-
- /**
- * execute the entrance transition.
- */
- final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
- @Override
- public void run() {
- mProgressBarManager.hide();
- onExecuteEntranceTransition();
- }
- };
-
- /**
- * execute onEntranceTransitionEnd.
- */
- final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
- @Override
- public void run() {
- onEntranceTransitionEnd();
- }
- };
-
- /**
- * either entrance transition completed or skipped
- */
- final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
-
- /**
- * Event fragment.onCreate()
- */
- final Event EVT_ON_CREATE = new Event("onCreate");
-
- /**
- * Event fragment.onViewCreated()
- */
- final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
-
- /**
- * Event for {@link #prepareEntranceTransition()} is called.
- */
- final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
-
- /**
- * Event for {@link #startEntranceTransition()} is called.
- */
- final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
-
- /**
- * Event for entrance transition is ended through Transition listener.
- */
- final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
-
- /**
- * Event for skipping entrance transition if not supported.
- */
- final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
- @Override
- public boolean canProceed() {
- return !TransitionHelper.systemSupportsEntranceTransitions();
- }
- };
-
- final StateMachine mStateMachine = new StateMachine();
-
- Object mEntranceTransition;
- final ProgressBarManager mProgressBarManager = new ProgressBarManager();
-
- @SuppressLint("ValidFragment")
- BaseFragment() {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- createStateMachineStates();
- createStateMachineTransitions();
- mStateMachine.start();
- super.onCreate(savedInstanceState);
- mStateMachine.fireEvent(EVT_ON_CREATE);
- }
-
- void createStateMachineStates() {
- mStateMachine.addState(STATE_START);
- mStateMachine.addState(STATE_ENTRANCE_INIT);
- mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
- mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
- mStateMachine.addState(STATE_ENTRANCE_PERFORM);
- mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
- mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
- }
-
- void createStateMachineTransitions() {
- mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
- mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
- COND_TRANSITION_NOT_SUPPORTED);
- mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
- EVT_ON_CREATEVIEW);
- mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
- EVT_PREPARE_ENTRANCE);
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
- EVT_ON_CREATEVIEW);
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_ENTRANCE_PERFORM,
- EVT_START_ENTRANCE);
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
- STATE_ENTRANCE_PERFORM);
- mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
- STATE_ENTRANCE_ON_ENDED,
- EVT_ENTRANCE_END);
- mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
- }
-
- /**
- * Enables entrance transition.<p>
- * Entrance transition is the standard slide-in transition that shows rows of data in
- * browse screen and details screen.
- * <p>
- * The method is ignored before LOLLIPOP (API21).
- * <p>
- * This method must be called in or
- * before onCreate(). Typically entrance transition should be enabled when savedInstance is
- * null so that fragment restored from instanceState does not run an extra entrance transition.
- * When the entrance transition is enabled, the fragment will make headers and content
- * hidden initially.
- * When data of rows are ready, app must call {@link #startEntranceTransition()} to kick off
- * the transition, otherwise the rows will be invisible forever.
- * <p>
- * It is similar to android:windowsEnterTransition and can be considered a late-executed
- * android:windowsEnterTransition controlled by app. There are two reasons that app needs it:
- * <li> Workaround the problem that activity transition is not available between launcher and
- * app. Browse activity must programmatically start the slide-in transition.</li>
- * <li> Separates DetailsOverviewRow transition from other rows transition. So that
- * the DetailsOverviewRow transition can be executed earlier without waiting for all rows
- * to be loaded.</li>
- * <p>
- * Transition object is returned by createEntranceTransition(). Typically the app does not need
- * override the default transition that browse and details provides.
- */
- public void prepareEntranceTransition() {
- mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
- }
-
- /**
- * Create entrance transition. Subclass can override to load transition from
- * resource or construct manually. Typically app does not need to
- * override the default transition that browse and details provides.
- */
- protected Object createEntranceTransition() {
- return null;
- }
-
- /**
- * Run entrance transition. Subclass may use TransitionManager to perform
- * go(Scene) or beginDelayedTransition(). App should not override the default
- * implementation of browse and details fragment.
- */
- protected void runEntranceTransition(Object entranceTransition) {
- }
-
- /**
- * Callback when entrance transition is prepared. This is when fragment should
- * stop user input and animations.
- */
- protected void onEntranceTransitionPrepare() {
- }
-
- /**
- * Callback when entrance transition is started. This is when fragment should
- * stop processing layout.
- */
- protected void onEntranceTransitionStart() {
- }
-
- /**
- * Callback when entrance transition is ended.
- */
- protected void onEntranceTransitionEnd() {
- }
-
- /**
- * When fragment finishes loading data, it should call startEntranceTransition()
- * to execute the entrance transition.
- * startEntranceTransition() will start transition only if both two conditions
- * are satisfied:
- * <li> prepareEntranceTransition() was called.</li>
- * <li> has not executed entrance transition yet.</li>
- * <p>
- * If startEntranceTransition() is called before onViewCreated(), it will be pending
- * and executed when view is created.
- */
- public void startEntranceTransition() {
- mStateMachine.fireEvent(EVT_START_ENTRANCE);
- }
-
- void onExecuteEntranceTransition() {
- // wait till views get their initial position before start transition
- final View view = getView();
- if (view == null) {
- // fragment view destroyed, transition not needed
- return;
- }
- view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- view.getViewTreeObserver().removeOnPreDrawListener(this);
- if (FragmentUtil.getContext(BaseFragment.this) == null || getView() == null) {
- // bail out if fragment is destroyed immediately after startEntranceTransition
- return true;
- }
- internalCreateEntranceTransition();
- onEntranceTransitionStart();
- if (mEntranceTransition != null) {
- runEntranceTransition(mEntranceTransition);
- } else {
- mStateMachine.fireEvent(EVT_ENTRANCE_END);
- }
- return false;
- }
- });
- view.invalidate();
- }
-
- void internalCreateEntranceTransition() {
- mEntranceTransition = createEntranceTransition();
- if (mEntranceTransition == null) {
- return;
- }
- TransitionHelper.addTransitionListener(mEntranceTransition, new TransitionListener() {
- @Override
- public void onTransitionEnd(Object transition) {
- mEntranceTransition = null;
- mStateMachine.fireEvent(EVT_ENTRANCE_END);
- }
- });
- }
-
- /**
- * Returns the {@link ProgressBarManager}.
- * @return The {@link ProgressBarManager}.
- */
- public final ProgressBarManager getProgressBarManager() {
- return mProgressBarManager;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
deleted file mode 100644
index 2d79f3e..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ /dev/null
@@ -1,302 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BaseRowSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.app.Fragment;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * An internal base class for a fragment containing a list of rows.
- */
-abstract class BaseRowFragment extends Fragment {
- private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
- private ObjectAdapter mAdapter;
- VerticalGridView mVerticalGridView;
- private PresenterSelector mPresenterSelector;
- final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
- int mSelectedPosition = -1;
- private boolean mPendingTransitionPrepare;
- private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
-
- abstract int getLayoutResourceId();
-
- private final OnChildViewHolderSelectedListener mRowSelectedListener =
- new OnChildViewHolderSelectedListener() {
- @Override
- public void onChildViewHolderSelected(RecyclerView parent,
- RecyclerView.ViewHolder view, int position, int subposition) {
- if (!mLateSelectionObserver.mIsLateSelection) {
- mSelectedPosition = position;
- onRowSelected(parent, view, position, subposition);
- }
- }
- };
-
- void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,
- int position, int subposition) {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = inflater.inflate(getLayoutResourceId(), container, false);
- mVerticalGridView = findGridViewFromRoot(view);
- if (mPendingTransitionPrepare) {
- mPendingTransitionPrepare = false;
- onTransitionPrepare();
- }
- return view;
- }
-
- VerticalGridView findGridViewFromRoot(View view) {
- return (VerticalGridView) view;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
- }
- setAdapterAndSelection();
- mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
- }
-
- /**
- * This class waits for the adapter to be updated before setting the selected
- * row.
- */
- private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {
- boolean mIsLateSelection = false;
-
- LateSelectionObserver() {
- }
-
- @Override
- public void onChanged() {
- performLateSelection();
- }
-
- @Override
- public void onItemRangeInserted(int positionStart, int itemCount) {
- performLateSelection();
- }
-
- void startLateSelection() {
- mIsLateSelection = true;
- mBridgeAdapter.registerAdapterDataObserver(this);
- }
-
- void performLateSelection() {
- clear();
- if (mVerticalGridView != null) {
- mVerticalGridView.setSelectedPosition(mSelectedPosition);
- }
- }
-
- void clear() {
- if (mIsLateSelection) {
- mIsLateSelection = false;
- mBridgeAdapter.unregisterAdapterDataObserver(this);
- }
- }
- }
-
- void setAdapterAndSelection() {
- if (mAdapter == null) {
- // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter
- // to RecyclerView, it will not be allowed to change "hasStableId" to true.
- return;
- }
- if (mVerticalGridView.getAdapter() != mBridgeAdapter) {
- // avoid extra layout if ItemBridgeAdapter was already set.
- mVerticalGridView.setAdapter(mBridgeAdapter);
- }
- // We don't set the selected position unless we've data in the adapter.
- boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;
- if (lateSelection) {
- mLateSelectionObserver.startLateSelection();
- } else if (mSelectedPosition >= 0) {
- mVerticalGridView.setSelectedPosition(mSelectedPosition);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mLateSelectionObserver.clear();
- mVerticalGridView = null;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
- }
-
- /**
- * Set the presenter selector used to create and bind views.
- */
- public final void setPresenterSelector(PresenterSelector presenterSelector) {
- mPresenterSelector = presenterSelector;
- updateAdapter();
- }
-
- /**
- * Get the presenter selector used to create and bind views.
- */
- public final PresenterSelector getPresenterSelector() {
- return mPresenterSelector;
- }
-
- /**
- * Sets the adapter that represents a list of rows.
- * @param rowsAdapter Adapter that represents list of rows.
- */
- public final void setAdapter(ObjectAdapter rowsAdapter) {
- mAdapter = rowsAdapter;
- updateAdapter();
- }
-
- /**
- * Returns the Adapter that represents list of rows.
- * @return Adapter that represents list of rows.
- */
- public final ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.
- * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.
- */
- public final ItemBridgeAdapter getBridgeAdapter() {
- return mBridgeAdapter;
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Gets position of currently selected row.
- * @return Position of currently selected row.
- */
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- if (mSelectedPosition == position) {
- return;
- }
- mSelectedPosition = position;
- if (mVerticalGridView != null) {
- if (mLateSelectionObserver.mIsLateSelection) {
- return;
- }
- if (smooth) {
- mVerticalGridView.setSelectedPositionSmooth(position);
- } else {
- mVerticalGridView.setSelectedPosition(position);
- }
- }
- }
-
- public final VerticalGridView getVerticalGridView() {
- return mVerticalGridView;
- }
-
- void updateAdapter() {
- mBridgeAdapter.setAdapter(mAdapter);
- mBridgeAdapter.setPresenter(mPresenterSelector);
-
- if (mVerticalGridView != null) {
- setAdapterAndSelection();
- }
- }
-
- Object getItem(Row row, int position) {
- if (row instanceof ListRow) {
- return ((ListRow) row).getAdapter().get(position);
- } else {
- return null;
- }
- }
-
- public boolean onTransitionPrepare() {
- if (mVerticalGridView != null) {
- mVerticalGridView.setAnimateChildLayout(false);
- mVerticalGridView.setScrollEnabled(false);
- return true;
- }
- mPendingTransitionPrepare = true;
- return false;
- }
-
- public void onTransitionStart() {
- if (mVerticalGridView != null) {
- mVerticalGridView.setPruneChild(false);
- mVerticalGridView.setLayoutFrozen(true);
- mVerticalGridView.setFocusSearchDisabled(true);
- }
- }
-
- public void onTransitionEnd() {
- // be careful that fragment might be destroyed before header transition ends.
- if (mVerticalGridView != null) {
- mVerticalGridView.setLayoutFrozen(false);
- mVerticalGridView.setAnimateChildLayout(true);
- mVerticalGridView.setPruneChild(true);
- mVerticalGridView.setFocusSearchDisabled(false);
- mVerticalGridView.setScrollEnabled(true);
- }
- }
-
- public void setAlignment(int windowAlignOffsetTop) {
- if (mVerticalGridView != null) {
- // align the top edge of item
- mVerticalGridView.setItemAlignmentOffset(0);
- mVerticalGridView.setItemAlignmentOffsetPercent(
- VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
-
- // align to a fixed position from top
- mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);
- mVerticalGridView.setWindowAlignmentOffsetPercent(
- VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
deleted file mode 100644
index dba78da..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v4.app.Fragment;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * An internal base class for a fragment containing a list of rows.
- */
-abstract class BaseRowSupportFragment extends Fragment {
- private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
- private ObjectAdapter mAdapter;
- VerticalGridView mVerticalGridView;
- private PresenterSelector mPresenterSelector;
- final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();
- int mSelectedPosition = -1;
- private boolean mPendingTransitionPrepare;
- private LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();
-
- abstract int getLayoutResourceId();
-
- private final OnChildViewHolderSelectedListener mRowSelectedListener =
- new OnChildViewHolderSelectedListener() {
- @Override
- public void onChildViewHolderSelected(RecyclerView parent,
- RecyclerView.ViewHolder view, int position, int subposition) {
- if (!mLateSelectionObserver.mIsLateSelection) {
- mSelectedPosition = position;
- onRowSelected(parent, view, position, subposition);
- }
- }
- };
-
- void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,
- int position, int subposition) {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = inflater.inflate(getLayoutResourceId(), container, false);
- mVerticalGridView = findGridViewFromRoot(view);
- if (mPendingTransitionPrepare) {
- mPendingTransitionPrepare = false;
- onTransitionPrepare();
- }
- return view;
- }
-
- VerticalGridView findGridViewFromRoot(View view) {
- return (VerticalGridView) view;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);
- }
- setAdapterAndSelection();
- mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);
- }
-
- /**
- * This class waits for the adapter to be updated before setting the selected
- * row.
- */
- private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {
- boolean mIsLateSelection = false;
-
- LateSelectionObserver() {
- }
-
- @Override
- public void onChanged() {
- performLateSelection();
- }
-
- @Override
- public void onItemRangeInserted(int positionStart, int itemCount) {
- performLateSelection();
- }
-
- void startLateSelection() {
- mIsLateSelection = true;
- mBridgeAdapter.registerAdapterDataObserver(this);
- }
-
- void performLateSelection() {
- clear();
- if (mVerticalGridView != null) {
- mVerticalGridView.setSelectedPosition(mSelectedPosition);
- }
- }
-
- void clear() {
- if (mIsLateSelection) {
- mIsLateSelection = false;
- mBridgeAdapter.unregisterAdapterDataObserver(this);
- }
- }
- }
-
- void setAdapterAndSelection() {
- if (mAdapter == null) {
- // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter
- // to RecyclerView, it will not be allowed to change "hasStableId" to true.
- return;
- }
- if (mVerticalGridView.getAdapter() != mBridgeAdapter) {
- // avoid extra layout if ItemBridgeAdapter was already set.
- mVerticalGridView.setAdapter(mBridgeAdapter);
- }
- // We don't set the selected position unless we've data in the adapter.
- boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;
- if (lateSelection) {
- mLateSelectionObserver.startLateSelection();
- } else if (mSelectedPosition >= 0) {
- mVerticalGridView.setSelectedPosition(mSelectedPosition);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mLateSelectionObserver.clear();
- mVerticalGridView = null;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
- }
-
- /**
- * Set the presenter selector used to create and bind views.
- */
- public final void setPresenterSelector(PresenterSelector presenterSelector) {
- mPresenterSelector = presenterSelector;
- updateAdapter();
- }
-
- /**
- * Get the presenter selector used to create and bind views.
- */
- public final PresenterSelector getPresenterSelector() {
- return mPresenterSelector;
- }
-
- /**
- * Sets the adapter that represents a list of rows.
- * @param rowsAdapter Adapter that represents list of rows.
- */
- public final void setAdapter(ObjectAdapter rowsAdapter) {
- mAdapter = rowsAdapter;
- updateAdapter();
- }
-
- /**
- * Returns the Adapter that represents list of rows.
- * @return Adapter that represents list of rows.
- */
- public final ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.
- * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.
- */
- public final ItemBridgeAdapter getBridgeAdapter() {
- return mBridgeAdapter;
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Gets position of currently selected row.
- * @return Position of currently selected row.
- */
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- if (mSelectedPosition == position) {
- return;
- }
- mSelectedPosition = position;
- if (mVerticalGridView != null) {
- if (mLateSelectionObserver.mIsLateSelection) {
- return;
- }
- if (smooth) {
- mVerticalGridView.setSelectedPositionSmooth(position);
- } else {
- mVerticalGridView.setSelectedPosition(position);
- }
- }
- }
-
- public final VerticalGridView getVerticalGridView() {
- return mVerticalGridView;
- }
-
- void updateAdapter() {
- mBridgeAdapter.setAdapter(mAdapter);
- mBridgeAdapter.setPresenter(mPresenterSelector);
-
- if (mVerticalGridView != null) {
- setAdapterAndSelection();
- }
- }
-
- Object getItem(Row row, int position) {
- if (row instanceof ListRow) {
- return ((ListRow) row).getAdapter().get(position);
- } else {
- return null;
- }
- }
-
- public boolean onTransitionPrepare() {
- if (mVerticalGridView != null) {
- mVerticalGridView.setAnimateChildLayout(false);
- mVerticalGridView.setScrollEnabled(false);
- return true;
- }
- mPendingTransitionPrepare = true;
- return false;
- }
-
- public void onTransitionStart() {
- if (mVerticalGridView != null) {
- mVerticalGridView.setPruneChild(false);
- mVerticalGridView.setLayoutFrozen(true);
- mVerticalGridView.setFocusSearchDisabled(true);
- }
- }
-
- public void onTransitionEnd() {
- // be careful that fragment might be destroyed before header transition ends.
- if (mVerticalGridView != null) {
- mVerticalGridView.setLayoutFrozen(false);
- mVerticalGridView.setAnimateChildLayout(true);
- mVerticalGridView.setPruneChild(true);
- mVerticalGridView.setFocusSearchDisabled(false);
- mVerticalGridView.setScrollEnabled(true);
- }
- }
-
- public void setAlignment(int windowAlignOffsetTop) {
- if (mVerticalGridView != null) {
- // align the top edge of item
- mVerticalGridView.setItemAlignmentOffset(0);
- mVerticalGridView.setItemAlignmentOffsetPercent(
- VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
-
- // align to a fixed position from top
- mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);
- mVerticalGridView.setWindowAlignmentOffsetPercent(
- VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
deleted file mode 100644
index 1f6ad29..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
+++ /dev/null
@@ -1,338 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrandedSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.SearchOrbView;
-import android.support.v17.leanback.widget.TitleHelper;
-import android.support.v17.leanback.widget.TitleViewAdapter;
-import android.app.Fragment;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Fragment class for managing search and branding using a view that implements
- * {@link TitleViewAdapter.Provider}.
- */
-public class BrandedFragment extends Fragment {
-
- // BUNDLE attribute for title is showing
- private static final String TITLE_SHOW = "titleShow";
-
- private boolean mShowingTitle = true;
- private CharSequence mTitle;
- private Drawable mBadgeDrawable;
- private View mTitleView;
- private TitleViewAdapter mTitleViewAdapter;
- private SearchOrbView.Colors mSearchAffordanceColors;
- private boolean mSearchAffordanceColorSet;
- private View.OnClickListener mExternalOnSearchClickedListener;
- private TitleHelper mTitleHelper;
-
- /**
- * Called by {@link #installTitleView(LayoutInflater, ViewGroup, Bundle)} to inflate
- * title view. Default implementation uses layout file lb_browse_title.
- * Subclass may override and use its own layout, the layout must have a descendant with id
- * browse_title_group that implements {@link TitleViewAdapter.Provider}. Subclass may return
- * null if no title is needed.
- *
- * @param inflater The LayoutInflater object that can be used to inflate
- * any views in the fragment,
- * @param parent Parent of title view.
- * @param savedInstanceState If non-null, this fragment is being re-constructed
- * from a previous saved state as given here.
- * @return Title view which must have a descendant with id browse_title_group that implements
- * {@link TitleViewAdapter.Provider}, or null for no title view.
- */
- public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
- Bundle savedInstanceState) {
- TypedValue typedValue = new TypedValue();
- boolean found = parent.getContext().getTheme().resolveAttribute(
- R.attr.browseTitleViewLayout, typedValue, true);
- return inflater.inflate(found ? typedValue.resourceId : R.layout.lb_browse_title,
- parent, false);
- }
-
- /**
- * Inflate title view and add to parent. This method should be called in
- * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.
- * @param inflater The LayoutInflater object that can be used to inflate
- * any views in the fragment,
- * @param parent Parent of title view.
- * @param savedInstanceState If non-null, this fragment is being re-constructed
- * from a previous saved state as given here.
- */
- public void installTitleView(LayoutInflater inflater, ViewGroup parent,
- Bundle savedInstanceState) {
- View titleLayoutRoot = onInflateTitleView(inflater, parent, savedInstanceState);
- if (titleLayoutRoot != null) {
- parent.addView(titleLayoutRoot);
- setTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group));
- } else {
- setTitleView(null);
- }
- }
-
- /**
- * Sets the view that implemented {@link TitleViewAdapter}.
- * @param titleView The view that implemented {@link TitleViewAdapter.Provider}.
- */
- public void setTitleView(View titleView) {
- mTitleView = titleView;
- if (mTitleView == null) {
- mTitleViewAdapter = null;
- mTitleHelper = null;
- } else {
- mTitleViewAdapter = ((TitleViewAdapter.Provider) mTitleView).getTitleViewAdapter();
- mTitleViewAdapter.setTitle(mTitle);
- mTitleViewAdapter.setBadgeDrawable(mBadgeDrawable);
- if (mSearchAffordanceColorSet) {
- mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);
- }
- if (mExternalOnSearchClickedListener != null) {
- setOnSearchClickedListener(mExternalOnSearchClickedListener);
- }
- if (getView() instanceof ViewGroup) {
- mTitleHelper = new TitleHelper((ViewGroup) getView(), mTitleView);
- }
- }
- }
-
- /**
- * Returns the view that implements {@link TitleViewAdapter.Provider}.
- * @return The view that implements {@link TitleViewAdapter.Provider}.
- */
- public View getTitleView() {
- return mTitleView;
- }
-
- /**
- * Returns the {@link TitleViewAdapter} implemented by title view.
- * @return The {@link TitleViewAdapter} implemented by title view.
- */
- public TitleViewAdapter getTitleViewAdapter() {
- return mTitleViewAdapter;
- }
-
- /**
- * Returns the {@link TitleHelper}.
- */
- TitleHelper getTitleHelper() {
- return mTitleHelper;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean(TITLE_SHOW, mShowingTitle);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- if (savedInstanceState != null) {
- mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);
- }
- if (mTitleView != null && view instanceof ViewGroup) {
- mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
- mTitleHelper.showTitle(mShowingTitle);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mTitleHelper = null;
- }
-
- /**
- * Shows or hides the title view.
- * @param show True to show title view, false to hide title view.
- */
- public void showTitle(boolean show) {
- // TODO: handle interruptions?
- if (show == mShowingTitle) {
- return;
- }
- mShowingTitle = show;
- if (mTitleHelper != null) {
- mTitleHelper.showTitle(show);
- }
- }
-
- /**
- * Changes title view's components visibility and shows title.
- * @param flags Flags representing the visibility of components inside title view.
- * @see TitleViewAdapter#SEARCH_VIEW_VISIBLE
- * @see TitleViewAdapter#BRANDING_VIEW_VISIBLE
- * @see TitleViewAdapter#FULL_VIEW_VISIBLE
- * @see TitleViewAdapter#updateComponentsVisibility(int)
- */
- public void showTitle(int flags) {
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.updateComponentsVisibility(flags);
- }
- showTitle(true);
- }
-
- /**
- * Sets the drawable displayed in the fragment title.
- *
- * @param drawable The Drawable to display in the fragment title.
- */
- public void setBadgeDrawable(Drawable drawable) {
- if (mBadgeDrawable != drawable) {
- mBadgeDrawable = drawable;
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setBadgeDrawable(drawable);
- }
- }
- }
-
- /**
- * Returns the badge drawable used in the fragment title.
- * @return The badge drawable used in the fragment title.
- */
- public Drawable getBadgeDrawable() {
- return mBadgeDrawable;
- }
-
- /**
- * Sets title text for the fragment.
- *
- * @param title The title text of the fragment.
- */
- public void setTitle(CharSequence title) {
- mTitle = title;
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setTitle(title);
- }
- }
-
- /**
- * Returns the title text for the fragment.
- * @return Title text for the fragment.
- */
- public CharSequence getTitle() {
- return mTitle;
- }
-
- /**
- * Sets a click listener for the search affordance.
- *
- * <p>The presence of a listener will change the visibility of the search
- * affordance in the fragment title. When set to non-null, the title will
- * contain an element that a user may click to begin a search.
- *
- * <p>The listener's {@link View.OnClickListener#onClick onClick} method
- * will be invoked when the user clicks on the search element.
- *
- * @param listener The listener to call when the search element is clicked.
- */
- public void setOnSearchClickedListener(View.OnClickListener listener) {
- mExternalOnSearchClickedListener = listener;
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setOnSearchClickedListener(listener);
- }
- }
-
- /**
- * Sets the {@link android.support.v17.leanback.widget.SearchOrbView.Colors} used to draw the
- * search affordance.
- *
- * @param colors Colors used to draw search affordance.
- */
- public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
- mSearchAffordanceColors = colors;
- mSearchAffordanceColorSet = true;
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);
- }
- }
-
- /**
- * Returns the {@link android.support.v17.leanback.widget.SearchOrbView.Colors}
- * used to draw the search affordance.
- */
- public SearchOrbView.Colors getSearchAffordanceColors() {
- if (mSearchAffordanceColorSet) {
- return mSearchAffordanceColors;
- }
- if (mTitleViewAdapter == null) {
- throw new IllegalStateException("Fragment views not yet created");
- }
- return mTitleViewAdapter.getSearchAffordanceColors();
- }
-
- /**
- * Sets the color used to draw the search affordance.
- * A default brighter color will be set by the framework.
- *
- * @param color The color to use for the search affordance.
- */
- public void setSearchAffordanceColor(int color) {
- setSearchAffordanceColors(new SearchOrbView.Colors(color));
- }
-
- /**
- * Returns the color used to draw the search affordance.
- */
- public int getSearchAffordanceColor() {
- return getSearchAffordanceColors().color;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- if (mTitleViewAdapter != null) {
- showTitle(mShowingTitle);
- mTitleViewAdapter.setAnimationEnabled(true);
- }
- }
-
- @Override
- public void onPause() {
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setAnimationEnabled(false);
- }
- super.onPause();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (mTitleViewAdapter != null) {
- mTitleViewAdapter.setAnimationEnabled(true);
- }
- }
-
- /**
- * Returns true/false to indicate the visibility of TitleView.
- *
- * @return boolean to indicate whether or not it's showing the title.
- */
- public final boolean isShowingTitle() {
- return mShowingTitle;
- }
-
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
deleted file mode 100644
index f377389..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ /dev/null
@@ -1,1816 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import static android.support.v7.widget.RecyclerView.NO_POSITION;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.support.annotation.ColorInt;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.transition.TransitionListener;
-import android.support.v17.leanback.util.StateMachine.Event;
-import android.support.v17.leanback.util.StateMachine.State;
-import android.support.v17.leanback.widget.BrowseFrameLayout;
-import android.support.v17.leanback.widget.InvisibleRowPresenter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.PageRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowHeaderPresenter;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.ScaleFrameLayout;
-import android.support.v17.leanback.widget.TitleViewAdapter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentManager.BackStackEntry;
-import android.app.FragmentTransaction;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.MarginLayoutParams;
-import android.view.ViewTreeObserver;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A fragment for creating Leanback browse screens. It is composed of a
- * RowsFragment and a HeadersFragment.
- * <p>
- * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
- * of rows in a vertical list. The elements in this adapter must be subclasses
- * of {@link Row}.
- * <p>
- * The HeadersFragment can be set to be either shown or hidden by default, or
- * may be disabled entirely. See {@link #setHeadersState} for details.
- * <p>
- * By default the BrowseFragment includes support for returning to the headers
- * when the user presses Back. For Activities that customize {@link
- * android.app.Activity#onBackPressed()}, you must disable this default Back key support by
- * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
- * use {@link BrowseFragment.BrowseTransitionListener} and
- * {@link #startHeadersTransition(boolean)}.
- * <p>
- * The recommended theme to use with a BrowseFragment is
- * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
- * </p>
- */
-public class BrowseFragment extends BaseFragment {
-
- // BUNDLE attribute for saving header show/hide status when backstack is used:
- static final String HEADER_STACK_INDEX = "headerStackIndex";
- // BUNDLE attribute for saving header show/hide status when backstack is not used:
- static final String HEADER_SHOW = "headerShow";
- private static final String IS_PAGE_ROW = "isPageRow";
- private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
-
- /**
- * State to hide headers fragment.
- */
- final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
- @Override
- public void run() {
- setEntranceTransitionStartState();
- }
- };
-
- /**
- * Event for Header fragment view is created, we could perform
- * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
- */
- final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
-
- /**
- * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
- * {@link #onEntranceTransitionPrepare()}.
- */
- final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
-
- /**
- * Event that data for the screen is ready, this is additional requirement to launch entrance
- * transition.
- */
- final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
-
- @Override
- void createStateMachineStates() {
- super.createStateMachineStates();
- mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
- }
-
- @Override
- void createStateMachineTransitions() {
- super.createStateMachineTransitions();
- // when headers fragment view is created we could setEntranceTransitionStartState()
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
- EVT_HEADER_VIEW_CREATED);
-
- // add additional requirement for onEntranceTransitionPrepare()
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
- EVT_MAIN_FRAGMENT_VIEW_CREATED);
- // add additional requirement to launch entrance transition.
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
- EVT_SCREEN_DATA_READY);
- }
-
- final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
- int mLastEntryCount;
- int mIndexOfHeadersBackStack;
-
- BackStackListener() {
- mLastEntryCount = getFragmentManager().getBackStackEntryCount();
- mIndexOfHeadersBackStack = -1;
- }
-
- void load(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
- mShowingHeaders = mIndexOfHeadersBackStack == -1;
- } else {
- if (!mShowingHeaders) {
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- }
- }
- }
-
- void save(Bundle outState) {
- outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
- }
-
-
- @Override
- public void onBackStackChanged() {
- if (getFragmentManager() == null) {
- Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
- return;
- }
- int count = getFragmentManager().getBackStackEntryCount();
- // if backstack is growing and last pushed entry is "headers" backstack,
- // remember the index of the entry.
- if (count > mLastEntryCount) {
- BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
- if (mWithHeadersBackStackName.equals(entry.getName())) {
- mIndexOfHeadersBackStack = count - 1;
- }
- } else if (count < mLastEntryCount) {
- // if popped "headers" backstack, initiate the show header transition if needed
- if (mIndexOfHeadersBackStack >= count) {
- if (!isHeadersDataReady()) {
- // if main fragment was restored first before BrowseFragment's adapter gets
- // restored: don't start header transition, but add the entry back.
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- return;
- }
- mIndexOfHeadersBackStack = -1;
- if (!mShowingHeaders) {
- startHeadersTransitionInternal(true);
- }
- }
- }
- mLastEntryCount = count;
- }
- }
-
- /**
- * Listener for transitions between browse headers and rows.
- */
- public static class BrowseTransitionListener {
- /**
- * Callback when headers transition starts.
- *
- * @param withHeaders True if the transition will result in headers
- * being shown, false otherwise.
- */
- public void onHeadersTransitionStart(boolean withHeaders) {
- }
- /**
- * Callback when headers transition stops.
- *
- * @param withHeaders True if the transition will result in headers
- * being shown, false otherwise.
- */
- public void onHeadersTransitionStop(boolean withHeaders) {
- }
- }
-
- private class SetSelectionRunnable implements Runnable {
- static final int TYPE_INVALID = -1;
- static final int TYPE_INTERNAL_SYNC = 0;
- static final int TYPE_USER_REQUEST = 1;
-
- private int mPosition;
- private int mType;
- private boolean mSmooth;
-
- SetSelectionRunnable() {
- reset();
- }
-
- void post(int position, int type, boolean smooth) {
- // Posting the set selection, rather than calling it immediately, prevents an issue
- // with adapter changes. Example: a row is added before the current selected row;
- // first the fast lane view updates its selection, then the rows fragment has that
- // new selection propagated immediately; THEN the rows view processes the same adapter
- // change and moves the selection again.
- if (type >= mType) {
- mPosition = position;
- mType = type;
- mSmooth = smooth;
- mBrowseFrame.removeCallbacks(this);
- mBrowseFrame.post(this);
- }
- }
-
- @Override
- public void run() {
- setSelection(mPosition, mSmooth);
- reset();
- }
-
- private void reset() {
- mPosition = -1;
- mType = TYPE_INVALID;
- mSmooth = false;
- }
- }
-
- /**
- * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom
- * fragments can interact with {@link BrowseFragment} using this interface.
- */
- public interface FragmentHost {
- /**
- * Fragments are required to invoke this callback once their view is created
- * inside {@link Fragment#onViewCreated} method. {@link BrowseFragment} starts the entrance
- * animation only after receiving this callback. Failure to invoke this method
- * will lead to fragment not showing up.
- *
- * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
- */
- void notifyViewCreated(MainFragmentAdapter fragmentAdapter);
-
- /**
- * Fragments mapped to {@link PageRow} are required to invoke this callback once their data
- * is created for transition, the entrance animation only after receiving this callback.
- * Failure to invoke this method will lead to fragment not showing up.
- *
- * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
- */
- void notifyDataReady(MainFragmentAdapter fragmentAdapter);
-
- /**
- * Show or hide title view in {@link BrowseFragment} for fragments mapped to
- * {@link PageRow}. Otherwise the request is ignored, in that case BrowseFragment is fully
- * in control of showing/hiding title view.
- * <p>
- * When HeadersFragment is visible, BrowseFragment will hide search affordance view if
- * there are other focusable rows above currently focused row.
- *
- * @param show Boolean indicating whether or not to show the title view.
- */
- void showTitleView(boolean show);
- }
-
- /**
- * Default implementation of {@link FragmentHost} that is used only by
- * {@link BrowseFragment}.
- */
- private final class FragmentHostImpl implements FragmentHost {
- boolean mShowTitleView = true;
-
- FragmentHostImpl() {
- }
-
- @Override
- public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
- mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
- if (!mIsPageRow) {
- // If it's not a PageRow: it's a ListRow, so we already have data ready.
- mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
- }
- }
-
- @Override
- public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
- // If fragment host is not the currently active fragment (in BrowseFragment), then
- // ignore the request.
- if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
- return;
- }
-
- // We only honor showTitle request for PageRows.
- if (!mIsPageRow) {
- return;
- }
-
- mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
- }
-
- @Override
- public void showTitleView(boolean show) {
- mShowTitleView = show;
-
- // If fragment host is not the currently active fragment (in BrowseFragment), then
- // ignore the request.
- if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
- return;
- }
-
- // We only honor showTitle request for PageRows.
- if (!mIsPageRow) {
- return;
- }
-
- updateTitleViewVisibility();
- }
- }
-
- /**
- * Interface that defines the interaction between {@link BrowseFragment} and its main
- * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
- * it will be used to get the fragment to be shown in the content section. Clients can
- * provide any implementation of fragment and customize its interaction with
- * {@link BrowseFragment} by overriding the necessary methods.
- *
- * <p>
- * Clients are expected to provide
- * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
- * implementations of {@link MainFragmentAdapter} for given content types. Currently
- * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
- * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
- * {@link PageRow} - {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}.
- *
- * <p>
- * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
- * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
- * and provide that through {@link MainFragmentAdapterRegistry}.
- * {@link MainFragmentAdapter} implementation can supply any fragment and override
- * just those interactions that makes sense.
- */
- public static class MainFragmentAdapter<T extends Fragment> {
- private boolean mScalingEnabled;
- private final T mFragment;
- FragmentHostImpl mFragmentHost;
-
- public MainFragmentAdapter(T fragment) {
- this.mFragment = fragment;
- }
-
- public final T getFragment() {
- return mFragment;
- }
-
- /**
- * Returns whether its scrolling.
- */
- public boolean isScrolling() {
- return false;
- }
-
- /**
- * Set the visibility of titles/hover card of browse rows.
- */
- public void setExpand(boolean expand) {
- }
-
- /**
- * For rows that willing to participate entrance transition, this function
- * hide views if afterTransition is true, show views if afterTransition is false.
- */
- public void setEntranceTransitionState(boolean state) {
- }
-
- /**
- * Sets the window alignment and also the pivots for scale operation.
- */
- public void setAlignment(int windowAlignOffsetFromTop) {
- }
-
- /**
- * Callback indicating transition prepare start.
- */
- public boolean onTransitionPrepare() {
- return false;
- }
-
- /**
- * Callback indicating transition start.
- */
- public void onTransitionStart() {
- }
-
- /**
- * Callback indicating transition end.
- */
- public void onTransitionEnd() {
- }
-
- /**
- * Returns whether row scaling is enabled.
- */
- public boolean isScalingEnabled() {
- return mScalingEnabled;
- }
-
- /**
- * Sets the row scaling property.
- */
- public void setScalingEnabled(boolean scalingEnabled) {
- this.mScalingEnabled = scalingEnabled;
- }
-
- /**
- * Returns the current host interface so that main fragment can interact with
- * {@link BrowseFragment}.
- */
- public final FragmentHost getFragmentHost() {
- return mFragmentHost;
- }
-
- void setFragmentHost(FragmentHostImpl fragmentHost) {
- this.mFragmentHost = fragmentHost;
- }
- }
-
- /**
- * Interface to be implemented by all fragments for providing an instance of
- * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided
- * against {@link PageRow} will need to implement this interface.
- */
- public interface MainFragmentAdapterProvider {
- /**
- * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment}
- * would use to communicate with the target fragment.
- */
- MainFragmentAdapter getMainFragmentAdapter();
- }
-
- /**
- * Interface to be implemented by {@link RowsFragment} and its subclasses for providing
- * an instance of {@link MainFragmentRowsAdapter}.
- */
- public interface MainFragmentRowsAdapterProvider {
- /**
- * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment}
- * would use to communicate with the target fragment.
- */
- MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- }
-
- /**
- * This is used to pass information to {@link RowsFragment} or its subclasses.
- * {@link BrowseFragment} uses this interface to pass row based interaction events to
- * the target fragment.
- */
- public static class MainFragmentRowsAdapter<T extends Fragment> {
- private final T mFragment;
-
- public MainFragmentRowsAdapter(T fragment) {
- if (fragment == null) {
- throw new IllegalArgumentException("Fragment can't be null");
- }
- this.mFragment = fragment;
- }
-
- public final T getFragment() {
- return mFragment;
- }
- /**
- * Set the visibility titles/hover of browse rows.
- */
- public void setAdapter(ObjectAdapter adapter) {
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- }
-
- /**
- * Selects a Row and perform an optional task on the Row.
- */
- public void setSelectedPosition(int rowPosition,
- boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- }
-
- /**
- * Selects a Row.
- */
- public void setSelectedPosition(int rowPosition, boolean smooth) {
- }
-
- /**
- * @return The position of selected row.
- */
- public int getSelectedPosition() {
- return 0;
- }
-
- /**
- * @param position Position of Row.
- * @return Row ViewHolder.
- */
- public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
- return null;
- }
- }
-
- private boolean createMainFragment(ObjectAdapter adapter, int position) {
- Object item = null;
- if (!mCanShowHeaders) {
- // when header is disabled, we can decide to use RowsFragment even no data.
- } else if (adapter == null || adapter.size() == 0) {
- return false;
- } else {
- if (position < 0) {
- position = 0;
- } else if (position >= adapter.size()) {
- throw new IllegalArgumentException(
- String.format("Invalid position %d requested", position));
- }
- item = adapter.get(position);
- }
-
- boolean oldIsPageRow = mIsPageRow;
- mIsPageRow = mCanShowHeaders && item instanceof PageRow;
- boolean swap;
-
- if (mMainFragment == null) {
- swap = true;
- } else {
- if (oldIsPageRow) {
- swap = true;
- } else {
- swap = mIsPageRow;
- }
- }
-
- if (swap) {
- mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
- if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {
- throw new IllegalArgumentException(
- "Fragment must implement MainFragmentAdapterProvider");
- }
-
- mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
- .getMainFragmentAdapter();
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- mIsPageRow = mMainFragmentRowsAdapter == null;
- } else {
- mMainFragmentRowsAdapter = null;
- }
- }
-
- return swap;
- }
-
- /**
- * Factory class responsible for creating fragment given the current item. {@link ListRow}
- * should return {@link RowsFragment} or its subclass whereas {@link PageRow}
- * can return any fragment class.
- */
- public abstract static class FragmentFactory<T extends Fragment> {
- public abstract T createFragment(Object row);
- }
-
- /**
- * FragmentFactory implementation for {@link ListRow}.
- */
- public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {
- @Override
- public RowsFragment createFragment(Object row) {
- return new RowsFragment();
- }
- }
-
- /**
- * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
- * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
- * handling {@link ListRow}. Developers can override that and also if they want to
- * use custom fragment, they can register a custom {@link FragmentFactory}
- * against {@link PageRow}.
- */
- public final static class MainFragmentAdapterRegistry {
- private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();
- private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
-
- public MainFragmentAdapterRegistry() {
- registerFragment(ListRow.class, sDefaultFragmentFactory);
- }
-
- public void registerFragment(Class rowClass, FragmentFactory factory) {
- mItemToFragmentFactoryMapping.put(rowClass, factory);
- }
-
- public Fragment createFragment(Object item) {
- FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :
- mItemToFragmentFactoryMapping.get(item.getClass());
- if (fragmentFactory == null && !(item instanceof PageRow)) {
- fragmentFactory = sDefaultFragmentFactory;
- }
-
- return fragmentFactory.createFragment(item);
- }
- }
-
- static final String TAG = "BrowseFragment";
-
- private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
-
- static boolean DEBUG = false;
-
- /** The headers fragment is enabled and shown by default. */
- public static final int HEADERS_ENABLED = 1;
-
- /** The headers fragment is enabled and hidden by default. */
- public static final int HEADERS_HIDDEN = 2;
-
- /** The headers fragment is disabled and will never be shown. */
- public static final int HEADERS_DISABLED = 3;
-
- private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =
- new MainFragmentAdapterRegistry();
- MainFragmentAdapter mMainFragmentAdapter;
- Fragment mMainFragment;
- HeadersFragment mHeadersFragment;
- private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
-
- private ObjectAdapter mAdapter;
- private PresenterSelector mAdapterPresenter;
- private PresenterSelector mWrappingPresenterSelector;
-
- private int mHeadersState = HEADERS_ENABLED;
- private int mBrandColor = Color.TRANSPARENT;
- private boolean mBrandColorSet;
-
- BrowseFrameLayout mBrowseFrame;
- private ScaleFrameLayout mScaleFrameLayout;
- boolean mHeadersBackStackEnabled = true;
- String mWithHeadersBackStackName;
- boolean mShowingHeaders = true;
- boolean mCanShowHeaders = true;
- private int mContainerListMarginStart;
- private int mContainerListAlignTop;
- private boolean mMainFragmentScaleEnabled = true;
- OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
- private OnItemViewClickedListener mOnItemViewClickedListener;
- private int mSelectedPosition = -1;
- private float mScaleFactor;
- boolean mIsPageRow;
-
- private PresenterSelector mHeaderPresenterSelector;
- private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
-
- // transition related:
- Object mSceneWithHeaders;
- Object mSceneWithoutHeaders;
- private Object mSceneAfterEntranceTransition;
- Object mHeadersTransition;
- BackStackListener mBackStackChangedListener;
- BrowseTransitionListener mBrowseTransitionListener;
-
- private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
- private static final String ARG_HEADERS_STATE =
- BrowseFragment.class.getCanonicalName() + ".headersState";
-
- /**
- * Creates arguments for a browse fragment.
- *
- * @param args The Bundle to place arguments into, or null if the method
- * should return a new Bundle.
- * @param title The title of the BrowseFragment.
- * @param headersState The initial state of the headers of the
- * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
- * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
- * @return A Bundle with the given arguments for creating a BrowseFragment.
- */
- public static Bundle createArgs(Bundle args, String title, int headersState) {
- if (args == null) {
- args = new Bundle();
- }
- args.putString(ARG_TITLE, title);
- args.putInt(ARG_HEADERS_STATE, headersState);
- return args;
- }
-
- /**
- * Sets the brand color for the browse fragment. The brand color is used as
- * the primary color for UI elements in the browse fragment. For example,
- * the background color of the headers fragment uses the brand color.
- *
- * @param color The color to use as the brand color of the fragment.
- */
- public void setBrandColor(@ColorInt int color) {
- mBrandColor = color;
- mBrandColorSet = true;
-
- if (mHeadersFragment != null) {
- mHeadersFragment.setBackgroundColor(mBrandColor);
- }
- }
-
- /**
- * Returns the brand color for the browse fragment.
- * The default is transparent.
- */
- @ColorInt
- public int getBrandColor() {
- return mBrandColor;
- }
-
- /**
- * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
- * DividerRow and PageRow.
- */
- private void createAndSetWrapperPresenter() {
- final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
- if (adapterPresenter == null) {
- throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
- }
- if (adapterPresenter == mAdapterPresenter) {
- return;
- }
- mAdapterPresenter = adapterPresenter;
-
- Presenter[] presenters = adapterPresenter.getPresenters();
- final Presenter invisibleRowPresenter = new InvisibleRowPresenter();
- final Presenter[] allPresenters = new Presenter[presenters.length + 1];
- System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);
- allPresenters[allPresenters.length - 1] = invisibleRowPresenter;
- mAdapter.setPresenterSelector(new PresenterSelector() {
- @Override
- public Presenter getPresenter(Object item) {
- Row row = (Row) item;
- if (row.isRenderedAsRowView()) {
- return adapterPresenter.getPresenter(item);
- } else {
- return invisibleRowPresenter;
- }
- }
-
- @Override
- public Presenter[] getPresenters() {
- return allPresenters;
- }
- });
- }
-
- /**
- * Sets the adapter containing the rows for the fragment.
- *
- * <p>The items referenced by the adapter must be be derived from
- * {@link Row}. These rows will be used by the rows fragment and the headers
- * fragment (if not disabled) to render the browse rows.
- *
- * @param adapter An ObjectAdapter for the browse rows. All items must
- * derive from {@link Row}.
- */
- public void setAdapter(ObjectAdapter adapter) {
- mAdapter = adapter;
- createAndSetWrapperPresenter();
- if (getView() == null) {
- return;
- }
- replaceMainFragment(mSelectedPosition);
-
- if (adapter != null) {
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter));
- }
- mHeadersFragment.setAdapter(adapter);
- }
- }
-
- public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
- return mMainFragmentAdapterRegistry;
- }
-
- /**
- * Returns the adapter containing the rows for the fragment.
- */
- public ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- mExternalOnItemViewSelectedListener = listener;
- }
-
- /**
- * Returns an item selection listener.
- */
- public OnItemViewSelectedListener getOnItemViewSelectedListener() {
- return mExternalOnItemViewSelectedListener;
- }
-
- /**
- * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
- * not been created yet or a different fragment is bound to it.
- *
- * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
- */
- public RowsFragment getRowsFragment() {
- if (mMainFragment instanceof RowsFragment) {
- return (RowsFragment) mMainFragment;
- }
-
- return null;
- }
-
- /**
- * @return Current main fragment or null if not created.
- */
- public Fragment getMainFragment() {
- return mMainFragment;
- }
-
- /**
- * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
- * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
- */
- public HeadersFragment getHeadersFragment() {
- return mHeadersFragment;
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- * OnItemViewClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- mOnItemViewClickedListener = listener;
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
- }
- }
-
- /**
- * Returns the item Clicked listener.
- */
- public OnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- /**
- * Starts a headers transition.
- *
- * <p>This method will begin a transition to either show or hide the
- * headers, depending on the value of withHeaders. If headers are disabled
- * for this browse fragment, this method will throw an exception.
- *
- * @param withHeaders True if the headers should transition to being shown,
- * false if the transition should result in headers being hidden.
- */
- public void startHeadersTransition(boolean withHeaders) {
- if (!mCanShowHeaders) {
- throw new IllegalStateException("Cannot start headers transition");
- }
- if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
- return;
- }
- startHeadersTransitionInternal(withHeaders);
- }
-
- /**
- * Returns true if the headers transition is currently running.
- */
- public boolean isInHeadersTransition() {
- return mHeadersTransition != null;
- }
-
- /**
- * Returns true if headers are shown.
- */
- public boolean isShowingHeaders() {
- return mShowingHeaders;
- }
-
- /**
- * Sets a listener for browse fragment transitions.
- *
- * @param listener The listener to call when a browse headers transition
- * begins or ends.
- */
- public void setBrowseTransitionListener(BrowseTransitionListener listener) {
- mBrowseTransitionListener = listener;
- }
-
- /**
- * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
- *
- * @param enable true to enable row scaling
- */
- @Deprecated
- public void enableRowScaling(boolean enable) {
- enableMainFragmentScaling(enable);
- }
-
- /**
- * Enables scaling of main fragment when headers are present. For the page/row fragment,
- * scaling is enabled only when both this method and
- * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
- *
- * @param enable true to enable row scaling
- */
- public void enableMainFragmentScaling(boolean enable) {
- mMainFragmentScaleEnabled = enable;
- }
-
- void startHeadersTransitionInternal(final boolean withHeaders) {
- if (getFragmentManager().isDestroyed()) {
- return;
- }
- if (!isHeadersDataReady()) {
- return;
- }
- mShowingHeaders = withHeaders;
- mMainFragmentAdapter.onTransitionPrepare();
- mMainFragmentAdapter.onTransitionStart();
- onExpandTransitionStart(!withHeaders, new Runnable() {
- @Override
- public void run() {
- mHeadersFragment.onTransitionPrepare();
- mHeadersFragment.onTransitionStart();
- createHeadersTransition();
- if (mBrowseTransitionListener != null) {
- mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
- }
- TransitionHelper.runTransition(
- withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
- if (mHeadersBackStackEnabled) {
- if (!withHeaders) {
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- } else {
- int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
- if (index >= 0) {
- BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
- getFragmentManager().popBackStackImmediate(entry.getId(),
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- }
- }
- }
- }
- });
- }
-
- boolean isVerticalScrolling() {
- // don't run transition
- return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
- }
-
-
- private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
- new BrowseFrameLayout.OnFocusSearchListener() {
- @Override
- public View onFocusSearch(View focused, int direction) {
- // if headers is running transition, focus stays
- if (mCanShowHeaders && isInHeadersTransition()) {
- return focused;
- }
- if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
-
- if (getTitleView() != null && focused != getTitleView()
- && direction == View.FOCUS_UP) {
- return getTitleView();
- }
- if (getTitleView() != null && getTitleView().hasFocus()
- && direction == View.FOCUS_DOWN) {
- return mCanShowHeaders && mShowingHeaders
- ? mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
- }
-
- boolean isRtl = ViewCompat.getLayoutDirection(focused)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
- int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
- if (mCanShowHeaders && direction == towardStart) {
- if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {
- return focused;
- }
- return mHeadersFragment.getVerticalGridView();
- } else if (direction == towardEnd) {
- if (isVerticalScrolling()) {
- return focused;
- } else if (mMainFragment != null && mMainFragment.getView() != null) {
- return mMainFragment.getView();
- }
- return focused;
- } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {
- // disable focus_down moving into PageFragment.
- return focused;
- } else {
- return null;
- }
- }
- };
-
- final boolean isHeadersDataReady() {
- return mAdapter != null && mAdapter.size() != 0;
- }
-
- private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
- new BrowseFrameLayout.OnChildFocusListener() {
-
- @Override
- public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- if (getChildFragmentManager().isDestroyed()) {
- return true;
- }
- // Make sure not changing focus when requestFocus() is called.
- if (mCanShowHeaders && mShowingHeaders) {
- if (mHeadersFragment != null && mHeadersFragment.getView() != null
- && mHeadersFragment.getView().requestFocus(
- direction, previouslyFocusedRect)) {
- return true;
- }
- }
- if (mMainFragment != null && mMainFragment.getView() != null
- && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
- return true;
- }
- return getTitleView() != null
- && getTitleView().requestFocus(direction, previouslyFocusedRect);
- }
-
- @Override
- public void onRequestChildFocus(View child, View focused) {
- if (getChildFragmentManager().isDestroyed()) {
- return;
- }
- if (!mCanShowHeaders || isInHeadersTransition()) return;
- int childId = child.getId();
- if (childId == R.id.browse_container_dock && mShowingHeaders) {
- startHeadersTransitionInternal(false);
- } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
- startHeadersTransitionInternal(true);
- }
- }
- };
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
- outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
-
- if (mBackStackChangedListener != null) {
- mBackStackChangedListener.save(outState);
- } else {
- outState.putBoolean(HEADER_SHOW, mShowingHeaders);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final Context context = FragmentUtil.getContext(BrowseFragment.this);
- TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
- mContainerListMarginStart = (int) ta.getDimension(
- R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
- .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
- mContainerListAlignTop = (int) ta.getDimension(
- R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
- .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
- ta.recycle();
-
- readArguments(getArguments());
-
- if (mCanShowHeaders) {
- if (mHeadersBackStackEnabled) {
- mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
- mBackStackChangedListener = new BackStackListener();
- getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
- mBackStackChangedListener.load(savedInstanceState);
- } else {
- if (savedInstanceState != null) {
- mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
- }
- }
- }
-
- mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
- }
-
- @Override
- public void onDestroyView() {
- mMainFragmentRowsAdapter = null;
- mMainFragmentAdapter = null;
- mMainFragment = null;
- mHeadersFragment = null;
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- if (mBackStackChangedListener != null) {
- getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
- }
- super.onDestroy();
- }
-
- /**
- * Creates a new {@link HeadersFragment} instance. Subclass of BrowseFragment may override and
- * return an instance of subclass of HeadersFragment, e.g. when app wants to replace presenter
- * to render HeaderItem.
- *
- * @return A new instance of {@link HeadersFragment} or its subclass.
- */
- public HeadersFragment onCreateHeadersFragment() {
- return new HeadersFragment();
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
-
- if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
- mHeadersFragment = onCreateHeadersFragment();
-
- createMainFragment(mAdapter, mSelectedPosition);
- FragmentTransaction ft = getChildFragmentManager().beginTransaction()
- .replace(R.id.browse_headers_dock, mHeadersFragment);
-
- if (mMainFragment != null) {
- ft.replace(R.id.scale_frame, mMainFragment);
- } else {
- // Empty adapter used to guard against lazy adapter loading. When this
- // fragment is instantiated, mAdapter might not have the data or might not
- // have been set. In either of those cases mFragmentAdapter will be null.
- // This way we can maintain the invariant that mMainFragmentAdapter is never
- // null and it avoids doing null checks all over the code.
- mMainFragmentAdapter = new MainFragmentAdapter(null);
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
- }
-
- ft.commit();
- } else {
- mHeadersFragment = (HeadersFragment) getChildFragmentManager()
- .findFragmentById(R.id.browse_headers_dock);
- mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
- mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
- .getMainFragmentAdapter();
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
-
- mIsPageRow = savedInstanceState != null
- && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
-
- mSelectedPosition = savedInstanceState != null
- ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
-
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- } else {
- mMainFragmentRowsAdapter = null;
- }
- }
-
- mHeadersFragment.setHeadersGone(!mCanShowHeaders);
- if (mHeaderPresenterSelector != null) {
- mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
- }
- mHeadersFragment.setAdapter(mAdapter);
- mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
- mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
-
- View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
-
- getProgressBarManager().setRootView((ViewGroup)root);
-
- mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
- mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
- mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
-
- installTitleView(inflater, mBrowseFrame, savedInstanceState);
-
- mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
- mScaleFrameLayout.setPivotX(0);
- mScaleFrameLayout.setPivotY(mContainerListAlignTop);
-
- setupMainFragment();
-
- if (mBrandColorSet) {
- mHeadersFragment.setBackgroundColor(mBrandColor);
- }
-
- mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- showHeaders(true);
- }
- });
- mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- showHeaders(false);
- }
- });
- mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- setEntranceTransitionEndState();
- }
- });
-
- return root;
- }
-
- private void setupMainFragment() {
- if (mMainFragmentRowsAdapter != null) {
- if (mAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter));
- }
- mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
- new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
- mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
- }
-
- void createHeadersTransition() {
- mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),
- mShowingHeaders
- ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
-
- TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
- @Override
- public void onTransitionStart(Object transition) {
- }
- @Override
- public void onTransitionEnd(Object transition) {
- mHeadersTransition = null;
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.onTransitionEnd();
- if (!mShowingHeaders && mMainFragment != null) {
- View mainFragmentView = mMainFragment.getView();
- if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
- mainFragmentView.requestFocus();
- }
- }
- }
- if (mHeadersFragment != null) {
- mHeadersFragment.onTransitionEnd();
- if (mShowingHeaders) {
- VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
- if (headerGridView != null && !headerGridView.hasFocus()) {
- headerGridView.requestFocus();
- }
- }
- }
-
- // Animate TitleView once header animation is complete.
- updateTitleViewVisibility();
-
- if (mBrowseTransitionListener != null) {
- mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
- }
- }
- });
- }
-
- void updateTitleViewVisibility() {
- if (!mShowingHeaders) {
- boolean showTitleView;
- if (mIsPageRow && mMainFragmentAdapter != null) {
- // page fragment case:
- showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
- } else {
- // regular row view case:
- showTitleView = isFirstRowWithContent(mSelectedPosition);
- }
- if (showTitleView) {
- showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);
- } else {
- showTitle(false);
- }
- } else {
- // when HeaderFragment is showing, showBranding and showSearch are slightly different
- boolean showBranding;
- boolean showSearch;
- if (mIsPageRow && mMainFragmentAdapter != null) {
- showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
- } else {
- showBranding = isFirstRowWithContent(mSelectedPosition);
- }
- showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);
- int flags = 0;
- if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;
- if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;
- if (flags != 0) {
- showTitle(flags);
- } else {
- showTitle(false);
- }
- }
- }
-
- boolean isFirstRowWithContentOrPageRow(int rowPosition) {
- if (mAdapter == null || mAdapter.size() == 0) {
- return true;
- }
- for (int i = 0; i < mAdapter.size(); i++) {
- final Row row = (Row) mAdapter.get(i);
- if (row.isRenderedAsRowView() || row instanceof PageRow) {
- return rowPosition == i;
- }
- }
- return true;
- }
-
- boolean isFirstRowWithContent(int rowPosition) {
- if (mAdapter == null || mAdapter.size() == 0) {
- return true;
- }
- for (int i = 0; i < mAdapter.size(); i++) {
- final Row row = (Row) mAdapter.get(i);
- if (row.isRenderedAsRowView()) {
- return rowPosition == i;
- }
- }
- return true;
- }
-
- /**
- * Sets the {@link PresenterSelector} used to render the row headers.
- *
- * @param headerPresenterSelector The PresenterSelector that will determine
- * the Presenter for each row header.
- */
- public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
- mHeaderPresenterSelector = headerPresenterSelector;
- if (mHeadersFragment != null) {
- mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
- }
- }
-
- private void setHeadersOnScreen(boolean onScreen) {
- MarginLayoutParams lp;
- View containerList;
- containerList = mHeadersFragment.getView();
- lp = (MarginLayoutParams) containerList.getLayoutParams();
- lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
- containerList.setLayoutParams(lp);
- }
-
- void showHeaders(boolean show) {
- if (DEBUG) Log.v(TAG, "showHeaders " + show);
- mHeadersFragment.setHeadersEnabled(show);
- setHeadersOnScreen(show);
- expandMainFragment(!show);
- }
-
- private void expandMainFragment(boolean expand) {
- MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
- params.setMarginStart(!expand ? mContainerListMarginStart : 0);
- mScaleFrameLayout.setLayoutParams(params);
- mMainFragmentAdapter.setExpand(expand);
-
- setMainFragmentAlignment();
- final float scaleFactor = !expand
- && mMainFragmentScaleEnabled
- && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
- mScaleFrameLayout.setLayoutScaleY(scaleFactor);
- mScaleFrameLayout.setChildScale(scaleFactor);
- }
-
- private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
- new HeadersFragment.OnHeaderClickedListener() {
- @Override
- public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
- if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
- return;
- }
- startHeadersTransitionInternal(false);
- mMainFragment.getView().requestFocus();
- }
- };
-
- class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
- MainFragmentRowsAdapter mMainFragmentRowsAdapter;
-
- public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
- mMainFragmentRowsAdapter = fragmentRowsAdapter;
- }
-
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- int position = mMainFragmentRowsAdapter.getSelectedPosition();
- if (DEBUG) Log.v(TAG, "row selected position " + position);
- onRowSelected(position);
- if (mExternalOnItemViewSelectedListener != null) {
- mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
- rowViewHolder, row);
- }
- }
- };
-
- private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
- new HeadersFragment.OnHeaderViewSelectedListener() {
- @Override
- public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
- int position = mHeadersFragment.getSelectedPosition();
- if (DEBUG) Log.v(TAG, "header selected position " + position);
- onRowSelected(position);
- }
- };
-
- void onRowSelected(int position) {
- if (position != mSelectedPosition) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
- }
- }
-
- void setSelection(int position, boolean smooth) {
- if (position == NO_POSITION) {
- return;
- }
-
- mSelectedPosition = position;
- if (mHeadersFragment == null || mMainFragmentAdapter == null) {
- // onDestroyView() called
- return;
- }
- mHeadersFragment.setSelectedPosition(position, smooth);
- replaceMainFragment(position);
-
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
- }
-
- updateTitleViewVisibility();
- }
-
- private void replaceMainFragment(int position) {
- if (createMainFragment(mAdapter, position)) {
- swapToMainFragment();
- expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
- setupMainFragment();
- }
- }
-
- private void swapToMainFragment() {
- final VerticalGridView gridView = mHeadersFragment.getVerticalGridView();
- if (isShowingHeaders() && gridView != null
- && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
- // if user is scrolling HeadersFragment, swap to empty fragment and wait scrolling
- // finishes.
- getChildFragmentManager().beginTransaction()
- .replace(R.id.scale_frame, new Fragment()).commit();
- gridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @SuppressWarnings("ReferenceEquality")
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- gridView.removeOnScrollListener(this);
- FragmentManager fm = getChildFragmentManager();
- Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);
- if (currentFragment != mMainFragment) {
- fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit();
- }
- }
- }
- });
- } else {
- // Otherwise swap immediately
- getChildFragmentManager().beginTransaction()
- .replace(R.id.scale_frame, mMainFragment).commit();
- }
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Gets position of currently selected row.
- * @return Position of currently selected row.
- */
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- /**
- * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.
- */
- public RowPresenter.ViewHolder getSelectedRowViewHolder() {
- if (mMainFragmentRowsAdapter != null) {
- int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();
- return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);
- }
- return null;
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
- }
-
- /**
- * Selects a Row and perform an optional task on the Row. For example
- * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
- * scrolls to 11th row and selects 6th item on that row. The method will be ignored if
- * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
- * ViewGroup, Bundle)}).
- *
- * @param rowPosition Which row to select.
- * @param smooth True to scroll to the row, false for no animation.
- * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers
- * fragment will be collapsed.
- */
- public void setSelectedPosition(int rowPosition, boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- if (mMainFragmentAdapterRegistry == null) {
- return;
- }
- if (rowHolderTask != null) {
- startHeadersTransition(false);
- }
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mHeadersFragment.setAlignment(mContainerListAlignTop);
- setMainFragmentAlignment();
-
- if (mCanShowHeaders && mShowingHeaders && mHeadersFragment != null
- && mHeadersFragment.getView() != null) {
- mHeadersFragment.getView().requestFocus();
- } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null
- && mMainFragment.getView() != null) {
- mMainFragment.getView().requestFocus();
- }
-
- if (mCanShowHeaders) {
- showHeaders(mShowingHeaders);
- }
-
- mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
- }
-
- private void onExpandTransitionStart(boolean expand, final Runnable callback) {
- if (expand) {
- callback.run();
- return;
- }
- // Run a "pre" layout when we go non-expand, in order to get the initial
- // positions of added rows.
- new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
- }
-
- private void setMainFragmentAlignment() {
- int alignOffset = mContainerListAlignTop;
- if (mMainFragmentScaleEnabled
- && mMainFragmentAdapter.isScalingEnabled()
- && mShowingHeaders) {
- alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
- }
- mMainFragmentAdapter.setAlignment(alignOffset);
- }
-
- /**
- * Enables/disables headers transition on back key support. This is enabled by
- * default. The BrowseFragment will add a back stack entry when headers are
- * showing. Running a headers transition when the back key is pressed only
- * works when the headers state is {@link #HEADERS_ENABLED} or
- * {@link #HEADERS_HIDDEN}.
- * <p>
- * NOTE: If an Activity has its own onBackPressed() handling, you must
- * disable this feature. You may use {@link #startHeadersTransition(boolean)}
- * and {@link BrowseTransitionListener} in your own back stack handling.
- */
- public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
- mHeadersBackStackEnabled = headersBackStackEnabled;
- }
-
- /**
- * Returns true if headers transition on back key support is enabled.
- */
- public final boolean isHeadersTransitionOnBackEnabled() {
- return mHeadersBackStackEnabled;
- }
-
- private void readArguments(Bundle args) {
- if (args == null) {
- return;
- }
- if (args.containsKey(ARG_TITLE)) {
- setTitle(args.getString(ARG_TITLE));
- }
- if (args.containsKey(ARG_HEADERS_STATE)) {
- setHeadersState(args.getInt(ARG_HEADERS_STATE));
- }
- }
-
- /**
- * Sets the state for the headers column in the browse fragment. Must be one
- * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
- * {@link #HEADERS_DISABLED}.
- *
- * @param headersState The state of the headers for the browse fragment.
- */
- public void setHeadersState(int headersState) {
- if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
- throw new IllegalArgumentException("Invalid headers state: " + headersState);
- }
- if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
-
- if (headersState != mHeadersState) {
- mHeadersState = headersState;
- switch (headersState) {
- case HEADERS_ENABLED:
- mCanShowHeaders = true;
- mShowingHeaders = true;
- break;
- case HEADERS_HIDDEN:
- mCanShowHeaders = true;
- mShowingHeaders = false;
- break;
- case HEADERS_DISABLED:
- mCanShowHeaders = false;
- mShowingHeaders = false;
- break;
- default:
- Log.w(TAG, "Unknown headers state: " + headersState);
- break;
- }
- if (mHeadersFragment != null) {
- mHeadersFragment.setHeadersGone(!mCanShowHeaders);
- }
- }
- }
-
- /**
- * Returns the state of the headers column in the browse fragment.
- */
- public int getHeadersState() {
- return mHeadersState;
- }
-
- @Override
- protected Object createEntranceTransition() {
- return TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),
- R.transition.lb_browse_entrance_transition);
- }
-
- @Override
- protected void runEntranceTransition(Object entranceTransition) {
- TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
- }
-
- @Override
- protected void onEntranceTransitionPrepare() {
- mHeadersFragment.onTransitionPrepare();
- mMainFragmentAdapter.setEntranceTransitionState(false);
- mMainFragmentAdapter.onTransitionPrepare();
- }
-
- @Override
- protected void onEntranceTransitionStart() {
- mHeadersFragment.onTransitionStart();
- mMainFragmentAdapter.onTransitionStart();
- }
-
- @Override
- protected void onEntranceTransitionEnd() {
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.onTransitionEnd();
- }
-
- if (mHeadersFragment != null) {
- mHeadersFragment.onTransitionEnd();
- }
- }
-
- void setSearchOrbViewOnScreen(boolean onScreen) {
- View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();
- if (searchOrbView != null) {
- MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
- lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
- searchOrbView.setLayoutParams(lp);
- }
- }
-
- void setEntranceTransitionStartState() {
- setHeadersOnScreen(false);
- setSearchOrbViewOnScreen(false);
- // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
- // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
- // one when setEntranceTransitionStartState() is called.
- }
-
- void setEntranceTransitionEndState() {
- setHeadersOnScreen(mShowingHeaders);
- setSearchOrbViewOnScreen(true);
- mMainFragmentAdapter.setEntranceTransitionState(true);
- }
-
- private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
-
- private final View mView;
- private final Runnable mCallback;
- private int mState;
- private MainFragmentAdapter mainFragmentAdapter;
-
- final static int STATE_INIT = 0;
- final static int STATE_FIRST_DRAW = 1;
- final static int STATE_SECOND_DRAW = 2;
-
- ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
- mView = view;
- mCallback = callback;
- mainFragmentAdapter = adapter;
- }
-
- void execute() {
- mView.getViewTreeObserver().addOnPreDrawListener(this);
- mainFragmentAdapter.setExpand(false);
- // always trigger onPreDraw even adapter setExpand() does nothing.
- mView.invalidate();
- mState = STATE_INIT;
- }
-
- @Override
- public boolean onPreDraw() {
- if (getView() == null || FragmentUtil.getContext(BrowseFragment.this) == null) {
- mView.getViewTreeObserver().removeOnPreDrawListener(this);
- return true;
- }
- if (mState == STATE_INIT) {
- mainFragmentAdapter.setExpand(true);
- // always trigger onPreDraw even adapter setExpand() does nothing.
- mView.invalidate();
- mState = STATE_FIRST_DRAW;
- } else if (mState == STATE_FIRST_DRAW) {
- mCallback.run();
- mView.getViewTreeObserver().removeOnPreDrawListener(this);
- mState = STATE_SECOND_DRAW;
- }
- return false;
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
deleted file mode 100644
index 03b3c8a..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ /dev/null
@@ -1,1813 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import static android.support.v7.widget.RecyclerView.NO_POSITION;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.support.annotation.ColorInt;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.transition.TransitionListener;
-import android.support.v17.leanback.util.StateMachine.Event;
-import android.support.v17.leanback.util.StateMachine.State;
-import android.support.v17.leanback.widget.BrowseFrameLayout;
-import android.support.v17.leanback.widget.InvisibleRowPresenter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.PageRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowHeaderPresenter;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.ScaleFrameLayout;
-import android.support.v17.leanback.widget.TitleViewAdapter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentManager.BackStackEntry;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.MarginLayoutParams;
-import android.view.ViewTreeObserver;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A fragment for creating Leanback browse screens. It is composed of a
- * RowsSupportFragment and a HeadersSupportFragment.
- * <p>
- * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set
- * of rows in a vertical list. The elements in this adapter must be subclasses
- * of {@link Row}.
- * <p>
- * The HeadersSupportFragment can be set to be either shown or hidden by default, or
- * may be disabled entirely. See {@link #setHeadersState} for details.
- * <p>
- * By default the BrowseSupportFragment includes support for returning to the headers
- * when the user presses Back. For Activities that customize {@link
- * android.support.v4.app.FragmentActivity#onBackPressed()}, you must disable this default Back key support by
- * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
- * use {@link BrowseSupportFragment.BrowseTransitionListener} and
- * {@link #startHeadersTransition(boolean)}.
- * <p>
- * The recommended theme to use with a BrowseSupportFragment is
- * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
- * </p>
- */
-public class BrowseSupportFragment extends BaseSupportFragment {
-
- // BUNDLE attribute for saving header show/hide status when backstack is used:
- static final String HEADER_STACK_INDEX = "headerStackIndex";
- // BUNDLE attribute for saving header show/hide status when backstack is not used:
- static final String HEADER_SHOW = "headerShow";
- private static final String IS_PAGE_ROW = "isPageRow";
- private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
-
- /**
- * State to hide headers fragment.
- */
- final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
- @Override
- public void run() {
- setEntranceTransitionStartState();
- }
- };
-
- /**
- * Event for Header fragment view is created, we could perform
- * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
- */
- final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
-
- /**
- * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
- * {@link #onEntranceTransitionPrepare()}.
- */
- final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
-
- /**
- * Event that data for the screen is ready, this is additional requirement to launch entrance
- * transition.
- */
- final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
-
- @Override
- void createStateMachineStates() {
- super.createStateMachineStates();
- mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
- }
-
- @Override
- void createStateMachineTransitions() {
- super.createStateMachineTransitions();
- // when headers fragment view is created we could setEntranceTransitionStartState()
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
- EVT_HEADER_VIEW_CREATED);
-
- // add additional requirement for onEntranceTransitionPrepare()
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
- EVT_MAIN_FRAGMENT_VIEW_CREATED);
- // add additional requirement to launch entrance transition.
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
- EVT_SCREEN_DATA_READY);
- }
-
- final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
- int mLastEntryCount;
- int mIndexOfHeadersBackStack;
-
- BackStackListener() {
- mLastEntryCount = getFragmentManager().getBackStackEntryCount();
- mIndexOfHeadersBackStack = -1;
- }
-
- void load(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
- mShowingHeaders = mIndexOfHeadersBackStack == -1;
- } else {
- if (!mShowingHeaders) {
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- }
- }
- }
-
- void save(Bundle outState) {
- outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
- }
-
-
- @Override
- public void onBackStackChanged() {
- if (getFragmentManager() == null) {
- Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
- return;
- }
- int count = getFragmentManager().getBackStackEntryCount();
- // if backstack is growing and last pushed entry is "headers" backstack,
- // remember the index of the entry.
- if (count > mLastEntryCount) {
- BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
- if (mWithHeadersBackStackName.equals(entry.getName())) {
- mIndexOfHeadersBackStack = count - 1;
- }
- } else if (count < mLastEntryCount) {
- // if popped "headers" backstack, initiate the show header transition if needed
- if (mIndexOfHeadersBackStack >= count) {
- if (!isHeadersDataReady()) {
- // if main fragment was restored first before BrowseSupportFragment's adapter gets
- // restored: don't start header transition, but add the entry back.
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- return;
- }
- mIndexOfHeadersBackStack = -1;
- if (!mShowingHeaders) {
- startHeadersTransitionInternal(true);
- }
- }
- }
- mLastEntryCount = count;
- }
- }
-
- /**
- * Listener for transitions between browse headers and rows.
- */
- public static class BrowseTransitionListener {
- /**
- * Callback when headers transition starts.
- *
- * @param withHeaders True if the transition will result in headers
- * being shown, false otherwise.
- */
- public void onHeadersTransitionStart(boolean withHeaders) {
- }
- /**
- * Callback when headers transition stops.
- *
- * @param withHeaders True if the transition will result in headers
- * being shown, false otherwise.
- */
- public void onHeadersTransitionStop(boolean withHeaders) {
- }
- }
-
- private class SetSelectionRunnable implements Runnable {
- static final int TYPE_INVALID = -1;
- static final int TYPE_INTERNAL_SYNC = 0;
- static final int TYPE_USER_REQUEST = 1;
-
- private int mPosition;
- private int mType;
- private boolean mSmooth;
-
- SetSelectionRunnable() {
- reset();
- }
-
- void post(int position, int type, boolean smooth) {
- // Posting the set selection, rather than calling it immediately, prevents an issue
- // with adapter changes. Example: a row is added before the current selected row;
- // first the fast lane view updates its selection, then the rows fragment has that
- // new selection propagated immediately; THEN the rows view processes the same adapter
- // change and moves the selection again.
- if (type >= mType) {
- mPosition = position;
- mType = type;
- mSmooth = smooth;
- mBrowseFrame.removeCallbacks(this);
- mBrowseFrame.post(this);
- }
- }
-
- @Override
- public void run() {
- setSelection(mPosition, mSmooth);
- reset();
- }
-
- private void reset() {
- mPosition = -1;
- mType = TYPE_INVALID;
- mSmooth = false;
- }
- }
-
- /**
- * Possible set of actions that {@link BrowseSupportFragment} exposes to clients. Custom
- * fragments can interact with {@link BrowseSupportFragment} using this interface.
- */
- public interface FragmentHost {
- /**
- * Fragments are required to invoke this callback once their view is created
- * inside {@link Fragment#onViewCreated} method. {@link BrowseSupportFragment} starts the entrance
- * animation only after receiving this callback. Failure to invoke this method
- * will lead to fragment not showing up.
- *
- * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
- */
- void notifyViewCreated(MainFragmentAdapter fragmentAdapter);
-
- /**
- * Fragments mapped to {@link PageRow} are required to invoke this callback once their data
- * is created for transition, the entrance animation only after receiving this callback.
- * Failure to invoke this method will lead to fragment not showing up.
- *
- * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
- */
- void notifyDataReady(MainFragmentAdapter fragmentAdapter);
-
- /**
- * Show or hide title view in {@link BrowseSupportFragment} for fragments mapped to
- * {@link PageRow}. Otherwise the request is ignored, in that case BrowseSupportFragment is fully
- * in control of showing/hiding title view.
- * <p>
- * When HeadersSupportFragment is visible, BrowseSupportFragment will hide search affordance view if
- * there are other focusable rows above currently focused row.
- *
- * @param show Boolean indicating whether or not to show the title view.
- */
- void showTitleView(boolean show);
- }
-
- /**
- * Default implementation of {@link FragmentHost} that is used only by
- * {@link BrowseSupportFragment}.
- */
- private final class FragmentHostImpl implements FragmentHost {
- boolean mShowTitleView = true;
-
- FragmentHostImpl() {
- }
-
- @Override
- public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
- mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
- if (!mIsPageRow) {
- // If it's not a PageRow: it's a ListRow, so we already have data ready.
- mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
- }
- }
-
- @Override
- public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
- // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
- // ignore the request.
- if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
- return;
- }
-
- // We only honor showTitle request for PageRows.
- if (!mIsPageRow) {
- return;
- }
-
- mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
- }
-
- @Override
- public void showTitleView(boolean show) {
- mShowTitleView = show;
-
- // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
- // ignore the request.
- if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
- return;
- }
-
- // We only honor showTitle request for PageRows.
- if (!mIsPageRow) {
- return;
- }
-
- updateTitleViewVisibility();
- }
- }
-
- /**
- * Interface that defines the interaction between {@link BrowseSupportFragment} and its main
- * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
- * it will be used to get the fragment to be shown in the content section. Clients can
- * provide any implementation of fragment and customize its interaction with
- * {@link BrowseSupportFragment} by overriding the necessary methods.
- *
- * <p>
- * Clients are expected to provide
- * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
- * implementations of {@link MainFragmentAdapter} for given content types. Currently
- * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
- * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
- * {@link PageRow} - {@link android.support.v17.leanback.app.RowsSupportFragment.MainFragmentAdapter}.
- *
- * <p>
- * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
- * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
- * and provide that through {@link MainFragmentAdapterRegistry}.
- * {@link MainFragmentAdapter} implementation can supply any fragment and override
- * just those interactions that makes sense.
- */
- public static class MainFragmentAdapter<T extends Fragment> {
- private boolean mScalingEnabled;
- private final T mFragment;
- FragmentHostImpl mFragmentHost;
-
- public MainFragmentAdapter(T fragment) {
- this.mFragment = fragment;
- }
-
- public final T getFragment() {
- return mFragment;
- }
-
- /**
- * Returns whether its scrolling.
- */
- public boolean isScrolling() {
- return false;
- }
-
- /**
- * Set the visibility of titles/hover card of browse rows.
- */
- public void setExpand(boolean expand) {
- }
-
- /**
- * For rows that willing to participate entrance transition, this function
- * hide views if afterTransition is true, show views if afterTransition is false.
- */
- public void setEntranceTransitionState(boolean state) {
- }
-
- /**
- * Sets the window alignment and also the pivots for scale operation.
- */
- public void setAlignment(int windowAlignOffsetFromTop) {
- }
-
- /**
- * Callback indicating transition prepare start.
- */
- public boolean onTransitionPrepare() {
- return false;
- }
-
- /**
- * Callback indicating transition start.
- */
- public void onTransitionStart() {
- }
-
- /**
- * Callback indicating transition end.
- */
- public void onTransitionEnd() {
- }
-
- /**
- * Returns whether row scaling is enabled.
- */
- public boolean isScalingEnabled() {
- return mScalingEnabled;
- }
-
- /**
- * Sets the row scaling property.
- */
- public void setScalingEnabled(boolean scalingEnabled) {
- this.mScalingEnabled = scalingEnabled;
- }
-
- /**
- * Returns the current host interface so that main fragment can interact with
- * {@link BrowseSupportFragment}.
- */
- public final FragmentHost getFragmentHost() {
- return mFragmentHost;
- }
-
- void setFragmentHost(FragmentHostImpl fragmentHost) {
- this.mFragmentHost = fragmentHost;
- }
- }
-
- /**
- * Interface to be implemented by all fragments for providing an instance of
- * {@link MainFragmentAdapter}. Both {@link RowsSupportFragment} and custom fragment provided
- * against {@link PageRow} will need to implement this interface.
- */
- public interface MainFragmentAdapterProvider {
- /**
- * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseSupportFragment}
- * would use to communicate with the target fragment.
- */
- MainFragmentAdapter getMainFragmentAdapter();
- }
-
- /**
- * Interface to be implemented by {@link RowsSupportFragment} and its subclasses for providing
- * an instance of {@link MainFragmentRowsAdapter}.
- */
- public interface MainFragmentRowsAdapterProvider {
- /**
- * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseSupportFragment}
- * would use to communicate with the target fragment.
- */
- MainFragmentRowsAdapter getMainFragmentRowsAdapter();
- }
-
- /**
- * This is used to pass information to {@link RowsSupportFragment} or its subclasses.
- * {@link BrowseSupportFragment} uses this interface to pass row based interaction events to
- * the target fragment.
- */
- public static class MainFragmentRowsAdapter<T extends Fragment> {
- private final T mFragment;
-
- public MainFragmentRowsAdapter(T fragment) {
- if (fragment == null) {
- throw new IllegalArgumentException("Fragment can't be null");
- }
- this.mFragment = fragment;
- }
-
- public final T getFragment() {
- return mFragment;
- }
- /**
- * Set the visibility titles/hover of browse rows.
- */
- public void setAdapter(ObjectAdapter adapter) {
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- }
-
- /**
- * Selects a Row and perform an optional task on the Row.
- */
- public void setSelectedPosition(int rowPosition,
- boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- }
-
- /**
- * Selects a Row.
- */
- public void setSelectedPosition(int rowPosition, boolean smooth) {
- }
-
- /**
- * @return The position of selected row.
- */
- public int getSelectedPosition() {
- return 0;
- }
-
- /**
- * @param position Position of Row.
- * @return Row ViewHolder.
- */
- public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
- return null;
- }
- }
-
- private boolean createMainFragment(ObjectAdapter adapter, int position) {
- Object item = null;
- if (!mCanShowHeaders) {
- // when header is disabled, we can decide to use RowsSupportFragment even no data.
- } else if (adapter == null || adapter.size() == 0) {
- return false;
- } else {
- if (position < 0) {
- position = 0;
- } else if (position >= adapter.size()) {
- throw new IllegalArgumentException(
- String.format("Invalid position %d requested", position));
- }
- item = adapter.get(position);
- }
-
- boolean oldIsPageRow = mIsPageRow;
- mIsPageRow = mCanShowHeaders && item instanceof PageRow;
- boolean swap;
-
- if (mMainFragment == null) {
- swap = true;
- } else {
- if (oldIsPageRow) {
- swap = true;
- } else {
- swap = mIsPageRow;
- }
- }
-
- if (swap) {
- mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
- if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {
- throw new IllegalArgumentException(
- "Fragment must implement MainFragmentAdapterProvider");
- }
-
- mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
- .getMainFragmentAdapter();
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- mIsPageRow = mMainFragmentRowsAdapter == null;
- } else {
- mMainFragmentRowsAdapter = null;
- }
- }
-
- return swap;
- }
-
- /**
- * Factory class responsible for creating fragment given the current item. {@link ListRow}
- * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow}
- * can return any fragment class.
- */
- public abstract static class FragmentFactory<T extends Fragment> {
- public abstract T createFragment(Object row);
- }
-
- /**
- * FragmentFactory implementation for {@link ListRow}.
- */
- public static class ListRowFragmentFactory extends FragmentFactory<RowsSupportFragment> {
- @Override
- public RowsSupportFragment createFragment(Object row) {
- return new RowsSupportFragment();
- }
- }
-
- /**
- * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
- * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
- * handling {@link ListRow}. Developers can override that and also if they want to
- * use custom fragment, they can register a custom {@link FragmentFactory}
- * against {@link PageRow}.
- */
- public final static class MainFragmentAdapterRegistry {
- private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();
- private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
-
- public MainFragmentAdapterRegistry() {
- registerFragment(ListRow.class, sDefaultFragmentFactory);
- }
-
- public void registerFragment(Class rowClass, FragmentFactory factory) {
- mItemToFragmentFactoryMapping.put(rowClass, factory);
- }
-
- public Fragment createFragment(Object item) {
- FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :
- mItemToFragmentFactoryMapping.get(item.getClass());
- if (fragmentFactory == null && !(item instanceof PageRow)) {
- fragmentFactory = sDefaultFragmentFactory;
- }
-
- return fragmentFactory.createFragment(item);
- }
- }
-
- static final String TAG = "BrowseSupportFragment";
-
- private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
-
- static boolean DEBUG = false;
-
- /** The headers fragment is enabled and shown by default. */
- public static final int HEADERS_ENABLED = 1;
-
- /** The headers fragment is enabled and hidden by default. */
- public static final int HEADERS_HIDDEN = 2;
-
- /** The headers fragment is disabled and will never be shown. */
- public static final int HEADERS_DISABLED = 3;
-
- private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =
- new MainFragmentAdapterRegistry();
- MainFragmentAdapter mMainFragmentAdapter;
- Fragment mMainFragment;
- HeadersSupportFragment mHeadersSupportFragment;
- private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
-
- private ObjectAdapter mAdapter;
- private PresenterSelector mAdapterPresenter;
- private PresenterSelector mWrappingPresenterSelector;
-
- private int mHeadersState = HEADERS_ENABLED;
- private int mBrandColor = Color.TRANSPARENT;
- private boolean mBrandColorSet;
-
- BrowseFrameLayout mBrowseFrame;
- private ScaleFrameLayout mScaleFrameLayout;
- boolean mHeadersBackStackEnabled = true;
- String mWithHeadersBackStackName;
- boolean mShowingHeaders = true;
- boolean mCanShowHeaders = true;
- private int mContainerListMarginStart;
- private int mContainerListAlignTop;
- private boolean mMainFragmentScaleEnabled = true;
- OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
- private OnItemViewClickedListener mOnItemViewClickedListener;
- private int mSelectedPosition = -1;
- private float mScaleFactor;
- boolean mIsPageRow;
-
- private PresenterSelector mHeaderPresenterSelector;
- private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
-
- // transition related:
- Object mSceneWithHeaders;
- Object mSceneWithoutHeaders;
- private Object mSceneAfterEntranceTransition;
- Object mHeadersTransition;
- BackStackListener mBackStackChangedListener;
- BrowseTransitionListener mBrowseTransitionListener;
-
- private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title";
- private static final String ARG_HEADERS_STATE =
- BrowseSupportFragment.class.getCanonicalName() + ".headersState";
-
- /**
- * Creates arguments for a browse fragment.
- *
- * @param args The Bundle to place arguments into, or null if the method
- * should return a new Bundle.
- * @param title The title of the BrowseSupportFragment.
- * @param headersState The initial state of the headers of the
- * BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
- * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
- * @return A Bundle with the given arguments for creating a BrowseSupportFragment.
- */
- public static Bundle createArgs(Bundle args, String title, int headersState) {
- if (args == null) {
- args = new Bundle();
- }
- args.putString(ARG_TITLE, title);
- args.putInt(ARG_HEADERS_STATE, headersState);
- return args;
- }
-
- /**
- * Sets the brand color for the browse fragment. The brand color is used as
- * the primary color for UI elements in the browse fragment. For example,
- * the background color of the headers fragment uses the brand color.
- *
- * @param color The color to use as the brand color of the fragment.
- */
- public void setBrandColor(@ColorInt int color) {
- mBrandColor = color;
- mBrandColorSet = true;
-
- if (mHeadersSupportFragment != null) {
- mHeadersSupportFragment.setBackgroundColor(mBrandColor);
- }
- }
-
- /**
- * Returns the brand color for the browse fragment.
- * The default is transparent.
- */
- @ColorInt
- public int getBrandColor() {
- return mBrandColor;
- }
-
- /**
- * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
- * DividerRow and PageRow.
- */
- private void createAndSetWrapperPresenter() {
- final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
- if (adapterPresenter == null) {
- throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
- }
- if (adapterPresenter == mAdapterPresenter) {
- return;
- }
- mAdapterPresenter = adapterPresenter;
-
- Presenter[] presenters = adapterPresenter.getPresenters();
- final Presenter invisibleRowPresenter = new InvisibleRowPresenter();
- final Presenter[] allPresenters = new Presenter[presenters.length + 1];
- System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);
- allPresenters[allPresenters.length - 1] = invisibleRowPresenter;
- mAdapter.setPresenterSelector(new PresenterSelector() {
- @Override
- public Presenter getPresenter(Object item) {
- Row row = (Row) item;
- if (row.isRenderedAsRowView()) {
- return adapterPresenter.getPresenter(item);
- } else {
- return invisibleRowPresenter;
- }
- }
-
- @Override
- public Presenter[] getPresenters() {
- return allPresenters;
- }
- });
- }
-
- /**
- * Sets the adapter containing the rows for the fragment.
- *
- * <p>The items referenced by the adapter must be be derived from
- * {@link Row}. These rows will be used by the rows fragment and the headers
- * fragment (if not disabled) to render the browse rows.
- *
- * @param adapter An ObjectAdapter for the browse rows. All items must
- * derive from {@link Row}.
- */
- public void setAdapter(ObjectAdapter adapter) {
- mAdapter = adapter;
- createAndSetWrapperPresenter();
- if (getView() == null) {
- return;
- }
- replaceMainFragment(mSelectedPosition);
-
- if (adapter != null) {
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter));
- }
- mHeadersSupportFragment.setAdapter(adapter);
- }
- }
-
- public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
- return mMainFragmentAdapterRegistry;
- }
-
- /**
- * Returns the adapter containing the rows for the fragment.
- */
- public ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- mExternalOnItemViewSelectedListener = listener;
- }
-
- /**
- * Returns an item selection listener.
- */
- public OnItemViewSelectedListener getOnItemViewSelectedListener() {
- return mExternalOnItemViewSelectedListener;
- }
-
- /**
- * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has
- * not been created yet or a different fragment is bound to it.
- *
- * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise.
- */
- public RowsSupportFragment getRowsSupportFragment() {
- if (mMainFragment instanceof RowsSupportFragment) {
- return (RowsSupportFragment) mMainFragment;
- }
-
- return null;
- }
-
- /**
- * @return Current main fragment or null if not created.
- */
- public Fragment getMainFragment() {
- return mMainFragment;
- }
-
- /**
- * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
- * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
- */
- public HeadersSupportFragment getHeadersSupportFragment() {
- return mHeadersSupportFragment;
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- * OnItemViewClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- mOnItemViewClickedListener = listener;
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
- }
- }
-
- /**
- * Returns the item Clicked listener.
- */
- public OnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- /**
- * Starts a headers transition.
- *
- * <p>This method will begin a transition to either show or hide the
- * headers, depending on the value of withHeaders. If headers are disabled
- * for this browse fragment, this method will throw an exception.
- *
- * @param withHeaders True if the headers should transition to being shown,
- * false if the transition should result in headers being hidden.
- */
- public void startHeadersTransition(boolean withHeaders) {
- if (!mCanShowHeaders) {
- throw new IllegalStateException("Cannot start headers transition");
- }
- if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
- return;
- }
- startHeadersTransitionInternal(withHeaders);
- }
-
- /**
- * Returns true if the headers transition is currently running.
- */
- public boolean isInHeadersTransition() {
- return mHeadersTransition != null;
- }
-
- /**
- * Returns true if headers are shown.
- */
- public boolean isShowingHeaders() {
- return mShowingHeaders;
- }
-
- /**
- * Sets a listener for browse fragment transitions.
- *
- * @param listener The listener to call when a browse headers transition
- * begins or ends.
- */
- public void setBrowseTransitionListener(BrowseTransitionListener listener) {
- mBrowseTransitionListener = listener;
- }
-
- /**
- * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead.
- *
- * @param enable true to enable row scaling
- */
- @Deprecated
- public void enableRowScaling(boolean enable) {
- enableMainFragmentScaling(enable);
- }
-
- /**
- * Enables scaling of main fragment when headers are present. For the page/row fragment,
- * scaling is enabled only when both this method and
- * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
- *
- * @param enable true to enable row scaling
- */
- public void enableMainFragmentScaling(boolean enable) {
- mMainFragmentScaleEnabled = enable;
- }
-
- void startHeadersTransitionInternal(final boolean withHeaders) {
- if (getFragmentManager().isDestroyed()) {
- return;
- }
- if (!isHeadersDataReady()) {
- return;
- }
- mShowingHeaders = withHeaders;
- mMainFragmentAdapter.onTransitionPrepare();
- mMainFragmentAdapter.onTransitionStart();
- onExpandTransitionStart(!withHeaders, new Runnable() {
- @Override
- public void run() {
- mHeadersSupportFragment.onTransitionPrepare();
- mHeadersSupportFragment.onTransitionStart();
- createHeadersTransition();
- if (mBrowseTransitionListener != null) {
- mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
- }
- TransitionHelper.runTransition(
- withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
- if (mHeadersBackStackEnabled) {
- if (!withHeaders) {
- getFragmentManager().beginTransaction()
- .addToBackStack(mWithHeadersBackStackName).commit();
- } else {
- int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
- if (index >= 0) {
- BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
- getFragmentManager().popBackStackImmediate(entry.getId(),
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- }
- }
- }
- }
- });
- }
-
- boolean isVerticalScrolling() {
- // don't run transition
- return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
- }
-
-
- private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
- new BrowseFrameLayout.OnFocusSearchListener() {
- @Override
- public View onFocusSearch(View focused, int direction) {
- // if headers is running transition, focus stays
- if (mCanShowHeaders && isInHeadersTransition()) {
- return focused;
- }
- if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
-
- if (getTitleView() != null && focused != getTitleView()
- && direction == View.FOCUS_UP) {
- return getTitleView();
- }
- if (getTitleView() != null && getTitleView().hasFocus()
- && direction == View.FOCUS_DOWN) {
- return mCanShowHeaders && mShowingHeaders
- ? mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView();
- }
-
- boolean isRtl = ViewCompat.getLayoutDirection(focused)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
- int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
- if (mCanShowHeaders && direction == towardStart) {
- if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {
- return focused;
- }
- return mHeadersSupportFragment.getVerticalGridView();
- } else if (direction == towardEnd) {
- if (isVerticalScrolling()) {
- return focused;
- } else if (mMainFragment != null && mMainFragment.getView() != null) {
- return mMainFragment.getView();
- }
- return focused;
- } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {
- // disable focus_down moving into PageFragment.
- return focused;
- } else {
- return null;
- }
- }
- };
-
- final boolean isHeadersDataReady() {
- return mAdapter != null && mAdapter.size() != 0;
- }
-
- private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
- new BrowseFrameLayout.OnChildFocusListener() {
-
- @Override
- public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- if (getChildFragmentManager().isDestroyed()) {
- return true;
- }
- // Make sure not changing focus when requestFocus() is called.
- if (mCanShowHeaders && mShowingHeaders) {
- if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null
- && mHeadersSupportFragment.getView().requestFocus(
- direction, previouslyFocusedRect)) {
- return true;
- }
- }
- if (mMainFragment != null && mMainFragment.getView() != null
- && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
- return true;
- }
- return getTitleView() != null
- && getTitleView().requestFocus(direction, previouslyFocusedRect);
- }
-
- @Override
- public void onRequestChildFocus(View child, View focused) {
- if (getChildFragmentManager().isDestroyed()) {
- return;
- }
- if (!mCanShowHeaders || isInHeadersTransition()) return;
- int childId = child.getId();
- if (childId == R.id.browse_container_dock && mShowingHeaders) {
- startHeadersTransitionInternal(false);
- } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
- startHeadersTransitionInternal(true);
- }
- }
- };
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
- outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
-
- if (mBackStackChangedListener != null) {
- mBackStackChangedListener.save(outState);
- } else {
- outState.putBoolean(HEADER_SHOW, mShowingHeaders);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final Context context = getContext();
- TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
- mContainerListMarginStart = (int) ta.getDimension(
- R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
- .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
- mContainerListAlignTop = (int) ta.getDimension(
- R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
- .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
- ta.recycle();
-
- readArguments(getArguments());
-
- if (mCanShowHeaders) {
- if (mHeadersBackStackEnabled) {
- mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
- mBackStackChangedListener = new BackStackListener();
- getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
- mBackStackChangedListener.load(savedInstanceState);
- } else {
- if (savedInstanceState != null) {
- mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
- }
- }
- }
-
- mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
- }
-
- @Override
- public void onDestroyView() {
- mMainFragmentRowsAdapter = null;
- mMainFragmentAdapter = null;
- mMainFragment = null;
- mHeadersSupportFragment = null;
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- if (mBackStackChangedListener != null) {
- getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
- }
- super.onDestroy();
- }
-
- /**
- * Creates a new {@link HeadersSupportFragment} instance. Subclass of BrowseSupportFragment may override and
- * return an instance of subclass of HeadersSupportFragment, e.g. when app wants to replace presenter
- * to render HeaderItem.
- *
- * @return A new instance of {@link HeadersSupportFragment} or its subclass.
- */
- public HeadersSupportFragment onCreateHeadersSupportFragment() {
- return new HeadersSupportFragment();
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
-
- if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
- mHeadersSupportFragment = onCreateHeadersSupportFragment();
-
- createMainFragment(mAdapter, mSelectedPosition);
- FragmentTransaction ft = getChildFragmentManager().beginTransaction()
- .replace(R.id.browse_headers_dock, mHeadersSupportFragment);
-
- if (mMainFragment != null) {
- ft.replace(R.id.scale_frame, mMainFragment);
- } else {
- // Empty adapter used to guard against lazy adapter loading. When this
- // fragment is instantiated, mAdapter might not have the data or might not
- // have been set. In either of those cases mFragmentAdapter will be null.
- // This way we can maintain the invariant that mMainFragmentAdapter is never
- // null and it avoids doing null checks all over the code.
- mMainFragmentAdapter = new MainFragmentAdapter(null);
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
- }
-
- ft.commit();
- } else {
- mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()
- .findFragmentById(R.id.browse_headers_dock);
- mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
- mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
- .getMainFragmentAdapter();
- mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
-
- mIsPageRow = savedInstanceState != null
- && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
-
- mSelectedPosition = savedInstanceState != null
- ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
-
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- } else {
- mMainFragmentRowsAdapter = null;
- }
- }
-
- mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
- if (mHeaderPresenterSelector != null) {
- mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
- }
- mHeadersSupportFragment.setAdapter(mAdapter);
- mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
- mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener);
-
- View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
-
- getProgressBarManager().setRootView((ViewGroup)root);
-
- mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
- mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
- mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
-
- installTitleView(inflater, mBrowseFrame, savedInstanceState);
-
- mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
- mScaleFrameLayout.setPivotX(0);
- mScaleFrameLayout.setPivotY(mContainerListAlignTop);
-
- setupMainFragment();
-
- if (mBrandColorSet) {
- mHeadersSupportFragment.setBackgroundColor(mBrandColor);
- }
-
- mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- showHeaders(true);
- }
- });
- mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- showHeaders(false);
- }
- });
- mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
- @Override
- public void run() {
- setEntranceTransitionEndState();
- }
- });
-
- return root;
- }
-
- private void setupMainFragment() {
- if (mMainFragmentRowsAdapter != null) {
- if (mAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter));
- }
- mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
- new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
- mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
- }
-
- void createHeadersTransition() {
- mHeadersTransition = TransitionHelper.loadTransition(getContext(),
- mShowingHeaders
- ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
-
- TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
- @Override
- public void onTransitionStart(Object transition) {
- }
- @Override
- public void onTransitionEnd(Object transition) {
- mHeadersTransition = null;
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.onTransitionEnd();
- if (!mShowingHeaders && mMainFragment != null) {
- View mainFragmentView = mMainFragment.getView();
- if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
- mainFragmentView.requestFocus();
- }
- }
- }
- if (mHeadersSupportFragment != null) {
- mHeadersSupportFragment.onTransitionEnd();
- if (mShowingHeaders) {
- VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView();
- if (headerGridView != null && !headerGridView.hasFocus()) {
- headerGridView.requestFocus();
- }
- }
- }
-
- // Animate TitleView once header animation is complete.
- updateTitleViewVisibility();
-
- if (mBrowseTransitionListener != null) {
- mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
- }
- }
- });
- }
-
- void updateTitleViewVisibility() {
- if (!mShowingHeaders) {
- boolean showTitleView;
- if (mIsPageRow && mMainFragmentAdapter != null) {
- // page fragment case:
- showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
- } else {
- // regular row view case:
- showTitleView = isFirstRowWithContent(mSelectedPosition);
- }
- if (showTitleView) {
- showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);
- } else {
- showTitle(false);
- }
- } else {
- // when HeaderFragment is showing, showBranding and showSearch are slightly different
- boolean showBranding;
- boolean showSearch;
- if (mIsPageRow && mMainFragmentAdapter != null) {
- showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
- } else {
- showBranding = isFirstRowWithContent(mSelectedPosition);
- }
- showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);
- int flags = 0;
- if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;
- if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;
- if (flags != 0) {
- showTitle(flags);
- } else {
- showTitle(false);
- }
- }
- }
-
- boolean isFirstRowWithContentOrPageRow(int rowPosition) {
- if (mAdapter == null || mAdapter.size() == 0) {
- return true;
- }
- for (int i = 0; i < mAdapter.size(); i++) {
- final Row row = (Row) mAdapter.get(i);
- if (row.isRenderedAsRowView() || row instanceof PageRow) {
- return rowPosition == i;
- }
- }
- return true;
- }
-
- boolean isFirstRowWithContent(int rowPosition) {
- if (mAdapter == null || mAdapter.size() == 0) {
- return true;
- }
- for (int i = 0; i < mAdapter.size(); i++) {
- final Row row = (Row) mAdapter.get(i);
- if (row.isRenderedAsRowView()) {
- return rowPosition == i;
- }
- }
- return true;
- }
-
- /**
- * Sets the {@link PresenterSelector} used to render the row headers.
- *
- * @param headerPresenterSelector The PresenterSelector that will determine
- * the Presenter for each row header.
- */
- public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
- mHeaderPresenterSelector = headerPresenterSelector;
- if (mHeadersSupportFragment != null) {
- mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
- }
- }
-
- private void setHeadersOnScreen(boolean onScreen) {
- MarginLayoutParams lp;
- View containerList;
- containerList = mHeadersSupportFragment.getView();
- lp = (MarginLayoutParams) containerList.getLayoutParams();
- lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
- containerList.setLayoutParams(lp);
- }
-
- void showHeaders(boolean show) {
- if (DEBUG) Log.v(TAG, "showHeaders " + show);
- mHeadersSupportFragment.setHeadersEnabled(show);
- setHeadersOnScreen(show);
- expandMainFragment(!show);
- }
-
- private void expandMainFragment(boolean expand) {
- MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
- params.setMarginStart(!expand ? mContainerListMarginStart : 0);
- mScaleFrameLayout.setLayoutParams(params);
- mMainFragmentAdapter.setExpand(expand);
-
- setMainFragmentAlignment();
- final float scaleFactor = !expand
- && mMainFragmentScaleEnabled
- && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
- mScaleFrameLayout.setLayoutScaleY(scaleFactor);
- mScaleFrameLayout.setChildScale(scaleFactor);
- }
-
- private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener =
- new HeadersSupportFragment.OnHeaderClickedListener() {
- @Override
- public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
- if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
- return;
- }
- startHeadersTransitionInternal(false);
- mMainFragment.getView().requestFocus();
- }
- };
-
- class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
- MainFragmentRowsAdapter mMainFragmentRowsAdapter;
-
- public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
- mMainFragmentRowsAdapter = fragmentRowsAdapter;
- }
-
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- int position = mMainFragmentRowsAdapter.getSelectedPosition();
- if (DEBUG) Log.v(TAG, "row selected position " + position);
- onRowSelected(position);
- if (mExternalOnItemViewSelectedListener != null) {
- mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
- rowViewHolder, row);
- }
- }
- };
-
- private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
- new HeadersSupportFragment.OnHeaderViewSelectedListener() {
- @Override
- public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
- int position = mHeadersSupportFragment.getSelectedPosition();
- if (DEBUG) Log.v(TAG, "header selected position " + position);
- onRowSelected(position);
- }
- };
-
- void onRowSelected(int position) {
- if (position != mSelectedPosition) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
- }
- }
-
- void setSelection(int position, boolean smooth) {
- if (position == NO_POSITION) {
- return;
- }
-
- mSelectedPosition = position;
- if (mHeadersSupportFragment == null || mMainFragmentAdapter == null) {
- // onDestroyView() called
- return;
- }
- mHeadersSupportFragment.setSelectedPosition(position, smooth);
- replaceMainFragment(position);
-
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
- }
-
- updateTitleViewVisibility();
- }
-
- private void replaceMainFragment(int position) {
- if (createMainFragment(mAdapter, position)) {
- swapToMainFragment();
- expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
- setupMainFragment();
- }
- }
-
- private void swapToMainFragment() {
- final VerticalGridView gridView = mHeadersSupportFragment.getVerticalGridView();
- if (isShowingHeaders() && gridView != null
- && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
- // if user is scrolling HeadersSupportFragment, swap to empty fragment and wait scrolling
- // finishes.
- getChildFragmentManager().beginTransaction()
- .replace(R.id.scale_frame, new Fragment()).commit();
- gridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @SuppressWarnings("ReferenceEquality")
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- gridView.removeOnScrollListener(this);
- FragmentManager fm = getChildFragmentManager();
- Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);
- if (currentFragment != mMainFragment) {
- fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit();
- }
- }
- }
- });
- } else {
- // Otherwise swap immediately
- getChildFragmentManager().beginTransaction()
- .replace(R.id.scale_frame, mMainFragment).commit();
- }
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Gets position of currently selected row.
- * @return Position of currently selected row.
- */
- public int getSelectedPosition() {
- return mSelectedPosition;
- }
-
- /**
- * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.
- */
- public RowPresenter.ViewHolder getSelectedRowViewHolder() {
- if (mMainFragmentRowsAdapter != null) {
- int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();
- return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);
- }
- return null;
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
- }
-
- /**
- * Selects a Row and perform an optional task on the Row. For example
- * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
- * scrolls to 11th row and selects 6th item on that row. The method will be ignored if
- * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
- * ViewGroup, Bundle)}).
- *
- * @param rowPosition Which row to select.
- * @param smooth True to scroll to the row, false for no animation.
- * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers
- * fragment will be collapsed.
- */
- public void setSelectedPosition(int rowPosition, boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- if (mMainFragmentAdapterRegistry == null) {
- return;
- }
- if (rowHolderTask != null) {
- startHeadersTransition(false);
- }
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mHeadersSupportFragment.setAlignment(mContainerListAlignTop);
- setMainFragmentAlignment();
-
- if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment != null
- && mHeadersSupportFragment.getView() != null) {
- mHeadersSupportFragment.getView().requestFocus();
- } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null
- && mMainFragment.getView() != null) {
- mMainFragment.getView().requestFocus();
- }
-
- if (mCanShowHeaders) {
- showHeaders(mShowingHeaders);
- }
-
- mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
- }
-
- private void onExpandTransitionStart(boolean expand, final Runnable callback) {
- if (expand) {
- callback.run();
- return;
- }
- // Run a "pre" layout when we go non-expand, in order to get the initial
- // positions of added rows.
- new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
- }
-
- private void setMainFragmentAlignment() {
- int alignOffset = mContainerListAlignTop;
- if (mMainFragmentScaleEnabled
- && mMainFragmentAdapter.isScalingEnabled()
- && mShowingHeaders) {
- alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
- }
- mMainFragmentAdapter.setAlignment(alignOffset);
- }
-
- /**
- * Enables/disables headers transition on back key support. This is enabled by
- * default. The BrowseSupportFragment will add a back stack entry when headers are
- * showing. Running a headers transition when the back key is pressed only
- * works when the headers state is {@link #HEADERS_ENABLED} or
- * {@link #HEADERS_HIDDEN}.
- * <p>
- * NOTE: If an Activity has its own onBackPressed() handling, you must
- * disable this feature. You may use {@link #startHeadersTransition(boolean)}
- * and {@link BrowseTransitionListener} in your own back stack handling.
- */
- public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
- mHeadersBackStackEnabled = headersBackStackEnabled;
- }
-
- /**
- * Returns true if headers transition on back key support is enabled.
- */
- public final boolean isHeadersTransitionOnBackEnabled() {
- return mHeadersBackStackEnabled;
- }
-
- private void readArguments(Bundle args) {
- if (args == null) {
- return;
- }
- if (args.containsKey(ARG_TITLE)) {
- setTitle(args.getString(ARG_TITLE));
- }
- if (args.containsKey(ARG_HEADERS_STATE)) {
- setHeadersState(args.getInt(ARG_HEADERS_STATE));
- }
- }
-
- /**
- * Sets the state for the headers column in the browse fragment. Must be one
- * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
- * {@link #HEADERS_DISABLED}.
- *
- * @param headersState The state of the headers for the browse fragment.
- */
- public void setHeadersState(int headersState) {
- if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
- throw new IllegalArgumentException("Invalid headers state: " + headersState);
- }
- if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
-
- if (headersState != mHeadersState) {
- mHeadersState = headersState;
- switch (headersState) {
- case HEADERS_ENABLED:
- mCanShowHeaders = true;
- mShowingHeaders = true;
- break;
- case HEADERS_HIDDEN:
- mCanShowHeaders = true;
- mShowingHeaders = false;
- break;
- case HEADERS_DISABLED:
- mCanShowHeaders = false;
- mShowingHeaders = false;
- break;
- default:
- Log.w(TAG, "Unknown headers state: " + headersState);
- break;
- }
- if (mHeadersSupportFragment != null) {
- mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
- }
- }
- }
-
- /**
- * Returns the state of the headers column in the browse fragment.
- */
- public int getHeadersState() {
- return mHeadersState;
- }
-
- @Override
- protected Object createEntranceTransition() {
- return TransitionHelper.loadTransition(getContext(),
- R.transition.lb_browse_entrance_transition);
- }
-
- @Override
- protected void runEntranceTransition(Object entranceTransition) {
- TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
- }
-
- @Override
- protected void onEntranceTransitionPrepare() {
- mHeadersSupportFragment.onTransitionPrepare();
- mMainFragmentAdapter.setEntranceTransitionState(false);
- mMainFragmentAdapter.onTransitionPrepare();
- }
-
- @Override
- protected void onEntranceTransitionStart() {
- mHeadersSupportFragment.onTransitionStart();
- mMainFragmentAdapter.onTransitionStart();
- }
-
- @Override
- protected void onEntranceTransitionEnd() {
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.onTransitionEnd();
- }
-
- if (mHeadersSupportFragment != null) {
- mHeadersSupportFragment.onTransitionEnd();
- }
- }
-
- void setSearchOrbViewOnScreen(boolean onScreen) {
- View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();
- if (searchOrbView != null) {
- MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
- lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
- searchOrbView.setLayoutParams(lp);
- }
- }
-
- void setEntranceTransitionStartState() {
- setHeadersOnScreen(false);
- setSearchOrbViewOnScreen(false);
- // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
- // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
- // one when setEntranceTransitionStartState() is called.
- }
-
- void setEntranceTransitionEndState() {
- setHeadersOnScreen(mShowingHeaders);
- setSearchOrbViewOnScreen(true);
- mMainFragmentAdapter.setEntranceTransitionState(true);
- }
-
- private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
-
- private final View mView;
- private final Runnable mCallback;
- private int mState;
- private MainFragmentAdapter mainFragmentAdapter;
-
- final static int STATE_INIT = 0;
- final static int STATE_FIRST_DRAW = 1;
- final static int STATE_SECOND_DRAW = 2;
-
- ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
- mView = view;
- mCallback = callback;
- mainFragmentAdapter = adapter;
- }
-
- void execute() {
- mView.getViewTreeObserver().addOnPreDrawListener(this);
- mainFragmentAdapter.setExpand(false);
- // always trigger onPreDraw even adapter setExpand() does nothing.
- mView.invalidate();
- mState = STATE_INIT;
- }
-
- @Override
- public boolean onPreDraw() {
- if (getView() == null || getContext() == null) {
- mView.getViewTreeObserver().removeOnPreDrawListener(this);
- return true;
- }
- if (mState == STATE_INIT) {
- mainFragmentAdapter.setExpand(true);
- // always trigger onPreDraw even adapter setExpand() does nothing.
- mView.invalidate();
- mState = STATE_FIRST_DRAW;
- } else if (mState == STATE_FIRST_DRAW) {
- mCallback.run();
- mView.getViewTreeObserver().removeOnPreDrawListener(this);
- mState = STATE_SECOND_DRAW;
- }
- return false;
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
deleted file mode 100644
index 3655963..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ /dev/null
@@ -1,932 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsSupportFragment.java. DO NOT MODIFY. */
-
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.transition.TransitionListener;
-import android.support.v17.leanback.util.StateMachine.Event;
-import android.support.v17.leanback.util.StateMachine.State;
-import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
-import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
-import android.support.v17.leanback.widget.BrowseFrameLayout;
-import android.support.v17.leanback.widget.DetailsParallax;
-import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.ItemAlignmentFacet;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-
-import java.lang.ref.WeakReference;
-
-/**
- * A fragment for creating Leanback details screens.
- *
- * <p>
- * A DetailsFragment renders the elements of its {@link ObjectAdapter} as a set
- * of rows in a vertical list.The Adapter's {@link PresenterSelector} must maintain subclasses
- * of {@link RowPresenter}.
- * </p>
- *
- * When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsFragment will
- * setup default behavior of the DetailsOverviewRow:
- * <li>
- * The alignment of FullWidthDetailsOverviewRowPresenter is setup in
- * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}.
- * </li>
- * <li>
- * The view status switching of FullWidthDetailsOverviewRowPresenter is done in
- * {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
- * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}.
- * </li>
- *
- * <p>
- * The recommended activity themes to use with a DetailsFragment are
- * <li>
- * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details} with activity
- * shared element transition for {@link FullWidthDetailsOverviewRowPresenter}.
- * </li>
- * <li>
- * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details_NoSharedElementTransition}
- * if shared element transition is not needed, for example if first row is not rendered by
- * {@link FullWidthDetailsOverviewRowPresenter}.
- * </li>
- * </p>
- *
- * <p>
- * DetailsFragment can use {@link DetailsFragmentBackgroundController} to add a parallax drawable
- * background and embedded video playing fragment.
- * </p>
- */
-public class DetailsFragment extends BaseFragment {
- static final String TAG = "DetailsFragment";
- static boolean DEBUG = false;
-
- final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
- @Override
- public void run() {
- mRowsFragment.setEntranceTransitionState(false);
- }
- };
-
- final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
-
- void switchToVideoBeforeVideoFragmentCreated() {
- // if the video fragment is not ready: immediately fade out covering drawable,
- // hide title and mark mPendingFocusOnVideo and set focus on it later.
- mDetailsBackgroundController.switchToVideoBeforeCreate();
- showTitle(false);
- mPendingFocusOnVideo = true;
- slideOutGridView();
- }
-
- final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
- false, false) {
- @Override
- public void run() {
- switchToVideoBeforeVideoFragmentCreated();
- }
- };
-
- final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
- false, false) {
- @Override
- public void run() {
- if (mWaitEnterTransitionTimeout != null) {
- mWaitEnterTransitionTimeout.mRef.clear();
- }
- // clear the activity enter/sharedElement transition, return transitions are kept.
- // keep the return transitions and clear enter transition
- if (getActivity() != null) {
- Window window = getActivity().getWindow();
- Object returnTransition = TransitionHelper.getReturnTransition(window);
- Object sharedReturnTransition = TransitionHelper
- .getSharedElementReturnTransition(window);
- TransitionHelper.setEnterTransition(window, null);
- TransitionHelper.setSharedElementEnterTransition(window, null);
- TransitionHelper.setReturnTransition(window, returnTransition);
- TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
- }
- }
- };
-
- final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
- true, false);
-
- final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
- @Override
- public void run() {
- Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
- TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
- }
- };
-
- final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
- @Override
- public void run() {
- if (mWaitEnterTransitionTimeout == null) {
- new WaitEnterTransitionTimeout(DetailsFragment.this);
- }
- }
- };
-
- /**
- * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
- * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
- */
- static class WaitEnterTransitionTimeout implements Runnable {
- static final long WAIT_ENTERTRANSITION_START = 200;
-
- final WeakReference<DetailsFragment> mRef;
-
- WaitEnterTransitionTimeout(DetailsFragment f) {
- mRef = new WeakReference<>(f);
- f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
- }
-
- @Override
- public void run() {
- DetailsFragment f = mRef.get();
- if (f != null) {
- f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
- }
- }
- }
-
- final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
- @Override
- public void run() {
- onSafeStart();
- }
- };
-
- final Event EVT_ONSTART = new Event("onStart");
-
- final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
-
- final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
-
- final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
-
- final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
-
- @Override
- void createStateMachineStates() {
- super.createStateMachineStates();
- mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
- mStateMachine.addState(STATE_ON_SAFE_START);
- mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
- mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
- mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
- mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
- mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
- mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
- }
-
- @Override
- void createStateMachineTransitions() {
- super.createStateMachineTransitions();
- /**
- * Part 1: Processing enter transitions after fragment.onCreate
- */
- mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
- // if transition is not supported, skip to complete
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
- COND_TRANSITION_NOT_SUPPORTED);
- // if transition is not set on Activity, skip to complete
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
- EVT_NO_ENTER_TRANSITION);
- // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
- // complete.
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
- EVT_SWITCH_TO_VIDEO);
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
- // once after onCreateView, we cannot skip the enter transition, add a listener and wait
- // it to finish
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
- EVT_ON_CREATEVIEW);
- // when enter transition finishes, go to complete, however this might never happen if
- // the activity is not giving transition options in startActivity, there is no API to query
- // if this activity is started in a enter transition mode. So we rely on a timer below:
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
- STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
- // we are expecting app to start delayed enter transition shortly after details row is
- // loaded, so create a timer and wait for enter transition start.
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
- STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
- // if enter transition not started in the timer, skip to DONE, this can be also true when
- // startActivity is not giving transition option.
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
- EVT_ENTER_TRANSIITON_DONE);
-
- /**
- * Part 2: modification to the entrance transition defined in BaseFragment
- */
- // Must finish enter transition before perform entrance transition.
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
- // Calling switch to video would hide immediately and skip entrance transition
- mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
- EVT_SWITCH_TO_VIDEO);
- mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
- // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
- // still need to do the switchToVideo.
- mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
- EVT_SWITCH_TO_VIDEO);
-
- // for once the view is created in onStart and prepareEntranceTransition was called, we
- // could setEntranceStartState:
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
-
- /**
- * Part 3: onSafeStart()
- */
- // for onSafeStart: the condition is onStart called, entrance transition complete
- mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
- mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
- mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
- }
-
- private class SetSelectionRunnable implements Runnable {
- int mPosition;
- boolean mSmooth = true;
-
- SetSelectionRunnable() {
- }
-
- @Override
- public void run() {
- if (mRowsFragment == null) {
- return;
- }
- mRowsFragment.setSelectedPosition(mPosition, mSmooth);
- }
- }
-
- TransitionListener mEnterTransitionListener = new TransitionListener() {
- @Override
- public void onTransitionStart(Object transition) {
- if (mWaitEnterTransitionTimeout != null) {
- // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
- // when transition finishes.
- mWaitEnterTransitionTimeout.mRef.clear();
- }
- }
-
- @Override
- public void onTransitionCancel(Object transition) {
- mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
- }
-
- @Override
- public void onTransitionEnd(Object transition) {
- mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
- }
- };
-
- TransitionListener mReturnTransitionListener = new TransitionListener() {
- @Override
- public void onTransitionStart(Object transition) {
- onReturnTransitionStart();
- }
- };
-
- BrowseFrameLayout mRootView;
- View mBackgroundView;
- Drawable mBackgroundDrawable;
- Fragment mVideoFragment;
- DetailsParallax mDetailsParallax;
- RowsFragment mRowsFragment;
- ObjectAdapter mAdapter;
- int mContainerListAlignTop;
- BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener;
- BaseOnItemViewClickedListener mOnItemViewClickedListener;
- DetailsFragmentBackgroundController mDetailsBackgroundController;
-
- // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
- // true, we will focus to VideoFragment immediately after video fragment's view is created.
- boolean mPendingFocusOnVideo = false;
-
- WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
-
- Object mSceneAfterEntranceTransition;
-
- final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
-
- final BaseOnItemViewSelectedListener<Object> mOnItemViewSelectedListener =
- new BaseOnItemViewSelectedListener<Object>() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Object row) {
- int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
- int subposition = mRowsFragment.getVerticalGridView().getSelectedSubPosition();
- if (DEBUG) Log.v(TAG, "row selected position " + position
- + " subposition " + subposition);
- onRowSelected(position, subposition);
- if (mExternalOnItemViewSelectedListener != null) {
- mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
- rowViewHolder, row);
- }
- }
- };
-
- /**
- * Sets the list of rows for the fragment.
- */
- public void setAdapter(ObjectAdapter adapter) {
- mAdapter = adapter;
- Presenter[] presenters = adapter.getPresenterSelector().getPresenters();
- if (presenters != null) {
- for (int i = 0; i < presenters.length; i++) {
- setupPresenter(presenters[i]);
- }
- } else {
- Log.e(TAG, "PresenterSelector.getPresenters() not implemented");
- }
- if (mRowsFragment != null) {
- mRowsFragment.setAdapter(adapter);
- }
- }
-
- /**
- * Returns the list of rows.
- */
- public ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
- mExternalOnItemViewSelectedListener = listener;
- }
-
- /**
- * Sets an item clicked listener.
- */
- public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
- if (mOnItemViewClickedListener != listener) {
- mOnItemViewClickedListener = listener;
- if (mRowsFragment != null) {
- mRowsFragment.setOnItemViewClickedListener(listener);
- }
- }
- }
-
- /**
- * Returns the item clicked listener.
- */
- public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mContainerListAlignTop =
- getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
-
- Activity activity = getActivity();
- if (activity != null) {
- Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
- if (transition == null) {
- mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
- }
- transition = TransitionHelper.getReturnTransition(activity.getWindow());
- if (transition != null) {
- TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
- }
- } else {
- mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mRootView = (BrowseFrameLayout) inflater.inflate(
- R.layout.lb_details_fragment, container, false);
- mBackgroundView = mRootView.findViewById(R.id.details_background_view);
- if (mBackgroundView != null) {
- mBackgroundView.setBackground(mBackgroundDrawable);
- }
- mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
- R.id.details_rows_dock);
- if (mRowsFragment == null) {
- mRowsFragment = new RowsFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.details_rows_dock, mRowsFragment).commit();
- }
- installTitleView(inflater, mRootView, savedInstanceState);
- mRowsFragment.setAdapter(mAdapter);
- mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
-
- mSceneAfterEntranceTransition = TransitionHelper.createScene(mRootView, new Runnable() {
- @Override
- public void run() {
- mRowsFragment.setEntranceTransitionState(true);
- }
- });
-
- setupDpadNavigation();
-
- if (Build.VERSION.SDK_INT >= 21) {
- // Setup adapter listener to work with ParallaxTransition (>= API 21).
- mRowsFragment.setExternalAdapterListener(new ItemBridgeAdapter.AdapterListener() {
- @Override
- public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
- if (mDetailsParallax != null && vh.getViewHolder()
- instanceof FullWidthDetailsOverviewRowPresenter.ViewHolder) {
- FullWidthDetailsOverviewRowPresenter.ViewHolder rowVh =
- (FullWidthDetailsOverviewRowPresenter.ViewHolder)
- vh.getViewHolder();
- rowVh.getOverviewView().setTag(R.id.lb_parallax_source,
- mDetailsParallax);
- }
- }
- });
- }
- return mRootView;
- }
-
- /**
- * @deprecated override {@link #onInflateTitleView(LayoutInflater,ViewGroup,Bundle)} instead.
- */
- @Deprecated
- protected View inflateTitle(LayoutInflater inflater, ViewGroup parent,
- Bundle savedInstanceState) {
- return super.onInflateTitleView(inflater, parent, savedInstanceState);
- }
-
- @Override
- public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
- Bundle savedInstanceState) {
- return inflateTitle(inflater, parent, savedInstanceState);
- }
-
- void setVerticalGridViewLayout(VerticalGridView listview) {
- // align the top edge of item to a fixed position
- listview.setItemAlignmentOffset(-mContainerListAlignTop);
- listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- listview.setWindowAlignmentOffset(0);
- listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- }
-
- /**
- * Called to setup each Presenter of Adapter passed in {@link #setAdapter(ObjectAdapter)}.Note
- * that setup should only change the Presenter behavior that is meaningful in DetailsFragment.
- * For example how a row is aligned in details Fragment. The default implementation invokes
- * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}
- *
- */
- protected void setupPresenter(Presenter rowPresenter) {
- if (rowPresenter instanceof FullWidthDetailsOverviewRowPresenter) {
- setupDetailsOverviewRowPresenter((FullWidthDetailsOverviewRowPresenter) rowPresenter);
- }
- }
-
- /**
- * Called to setup {@link FullWidthDetailsOverviewRowPresenter}. The default implementation
- * adds two alignment positions({@link ItemAlignmentFacet}) for ViewHolder of
- * FullWidthDetailsOverviewRowPresenter to align in fragment.
- */
- protected void setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter presenter) {
- ItemAlignmentFacet facet = new ItemAlignmentFacet();
- // by default align details_frame to half window height
- ItemAlignmentFacet.ItemAlignmentDef alignDef1 = new ItemAlignmentFacet.ItemAlignmentDef();
- alignDef1.setItemAlignmentViewId(R.id.details_frame);
- alignDef1.setItemAlignmentOffset(- getResources()
- .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions));
- alignDef1.setItemAlignmentOffsetPercent(0);
- // when description is selected, align details_frame to top edge
- ItemAlignmentFacet.ItemAlignmentDef alignDef2 = new ItemAlignmentFacet.ItemAlignmentDef();
- alignDef2.setItemAlignmentViewId(R.id.details_frame);
- alignDef2.setItemAlignmentFocusViewId(R.id.details_overview_description);
- alignDef2.setItemAlignmentOffset(- getResources()
- .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description));
- alignDef2.setItemAlignmentOffsetPercent(0);
- ItemAlignmentFacet.ItemAlignmentDef[] defs =
- new ItemAlignmentFacet.ItemAlignmentDef[] {alignDef1, alignDef2};
- facet.setAlignmentDefs(defs);
- presenter.setFacet(ItemAlignmentFacet.class, facet);
- }
-
- VerticalGridView getVerticalGridView() {
- return mRowsFragment == null ? null : mRowsFragment.getVerticalGridView();
- }
-
- /**
- * Gets embedded RowsFragment showing multiple rows for DetailsFragment. If view of
- * DetailsFragment is not created, the method returns null.
- * @return Embedded RowsFragment showing multiple rows for DetailsFragment.
- */
- public RowsFragment getRowsFragment() {
- return mRowsFragment;
- }
-
- /**
- * Setup dimensions that are only meaningful when the child Fragments are inside
- * DetailsFragment.
- */
- private void setupChildFragmentLayout() {
- setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- mSetSelectionRunnable.mPosition = position;
- mSetSelectionRunnable.mSmooth = smooth;
- if (getView() != null && getView().getHandler() != null) {
- getView().getHandler().post(mSetSelectionRunnable);
- }
- }
-
- void switchToVideo() {
- if (mVideoFragment != null && mVideoFragment.getView() != null) {
- mVideoFragment.getView().requestFocus();
- } else {
- mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
- }
- }
-
- void switchToRows() {
- mPendingFocusOnVideo = false;
- VerticalGridView verticalGridView = getVerticalGridView();
- if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
- verticalGridView.requestFocus();
- }
- }
-
- /**
- * This method asks DetailsFragmentBackgroundController to add a fragment for rendering video.
- * In case the fragment is already there, it will return the existing one. The method must be
- * called after calling super.onCreate(). App usually does not call this method directly.
- *
- * @return Fragment the added or restored fragment responsible for rendering video.
- * @see DetailsFragmentBackgroundController#onCreateVideoFragment()
- */
- final Fragment findOrCreateVideoFragment() {
- if (mVideoFragment != null) {
- return mVideoFragment;
- }
- Fragment fragment = getChildFragmentManager()
- .findFragmentById(R.id.video_surface_container);
- if (fragment == null && mDetailsBackgroundController != null) {
- FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
- ft2.add(android.support.v17.leanback.R.id.video_surface_container,
- fragment = mDetailsBackgroundController.onCreateVideoFragment());
- ft2.commit();
- if (mPendingFocusOnVideo) {
- // wait next cycle for Fragment view created so we can focus on it.
- // This is a bit hack eventually we will do commitNow() which get view immediately.
- getView().post(new Runnable() {
- @Override
- public void run() {
- if (getView() != null) {
- switchToVideo();
- }
- mPendingFocusOnVideo = false;
- }
- });
- }
- }
- mVideoFragment = fragment;
- return mVideoFragment;
- }
-
- void onRowSelected(int selectedPosition, int selectedSubPosition) {
- ObjectAdapter adapter = getAdapter();
- if (( mRowsFragment != null && mRowsFragment.getView() != null
- && mRowsFragment.getView().hasFocus() && !mPendingFocusOnVideo)
- && (adapter == null || adapter.size() == 0
- || (getVerticalGridView().getSelectedPosition() == 0
- && getVerticalGridView().getSelectedSubPosition() == 0))) {
- showTitle(true);
- } else {
- showTitle(false);
- }
- if (adapter != null && adapter.size() > selectedPosition) {
- final VerticalGridView gridView = getVerticalGridView();
- final int count = gridView.getChildCount();
- if (count > 0) {
- mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
- }
- for (int i = 0; i < count; i++) {
- ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
- gridView.getChildViewHolder(gridView.getChildAt(i));
- RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter();
- onSetRowStatus(rowPresenter,
- rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()),
- bridgeViewHolder.getAdapterPosition(),
- selectedPosition, selectedSubPosition);
- }
- }
- }
-
- /**
- * Called when onStart and enter transition (postponed/none postponed) and entrance transition
- * are all finished.
- */
- @CallSuper
- void onSafeStart() {
- if (mDetailsBackgroundController != null) {
- mDetailsBackgroundController.onStart();
- }
- }
-
- @CallSuper
- void onReturnTransitionStart() {
- if (mDetailsBackgroundController != null) {
- // first disable parallax effect that auto-start PlaybackGlue.
- boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
- // if video is not visible we can safely remove VideoFragment,
- // otherwise let video playing during return transition.
- if (!isVideoVisible && mVideoFragment != null) {
- FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
- ft2.remove(mVideoFragment);
- ft2.commit();
- mVideoFragment = null;
- }
- }
- }
-
- @Override
- public void onStop() {
- if (mDetailsBackgroundController != null) {
- mDetailsBackgroundController.onStop();
- }
- super.onStop();
- }
-
- /**
- * Called on every visible row to change view status when current selected row position
- * or selected sub position changed. Subclass may override. The default
- * implementation calls {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
- * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)} if presenter is
- * instance of {@link FullWidthDetailsOverviewRowPresenter}.
- *
- * @param presenter The presenter used to create row ViewHolder.
- * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not
- * be selected.
- * @param adapterPosition The adapter position of viewHolder inside adapter.
- * @param selectedPosition The adapter position of currently selected row.
- * @param selectedSubPosition The sub position within currently selected row. This is used
- * When a row has multiple alignment positions.
- */
- protected void onSetRowStatus(RowPresenter presenter, RowPresenter.ViewHolder viewHolder, int
- adapterPosition, int selectedPosition, int selectedSubPosition) {
- if (presenter instanceof FullWidthDetailsOverviewRowPresenter) {
- onSetDetailsOverviewRowStatus((FullWidthDetailsOverviewRowPresenter) presenter,
- (FullWidthDetailsOverviewRowPresenter.ViewHolder) viewHolder,
- adapterPosition, selectedPosition, selectedSubPosition);
- }
- }
-
- /**
- * Called to change DetailsOverviewRow view status when current selected row position
- * or selected sub position changed. Subclass may override. The default
- * implementation switches between three states based on the positions:
- * {@link FullWidthDetailsOverviewRowPresenter#STATE_HALF},
- * {@link FullWidthDetailsOverviewRowPresenter#STATE_FULL} and
- * {@link FullWidthDetailsOverviewRowPresenter#STATE_SMALL}.
- *
- * @param presenter The presenter used to create row ViewHolder.
- * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not
- * be selected.
- * @param adapterPosition The adapter position of viewHolder inside adapter.
- * @param selectedPosition The adapter position of currently selected row.
- * @param selectedSubPosition The sub position within currently selected row. This is used
- * When a row has multiple alignment positions.
- */
- protected void onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter presenter,
- FullWidthDetailsOverviewRowPresenter.ViewHolder viewHolder, int adapterPosition,
- int selectedPosition, int selectedSubPosition) {
- if (selectedPosition > adapterPosition) {
- presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
- } else if (selectedPosition == adapterPosition && selectedSubPosition == 1) {
- presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF);
- } else if (selectedPosition == adapterPosition && selectedSubPosition == 0){
- presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_FULL);
- } else {
- presenter.setState(viewHolder,
- FullWidthDetailsOverviewRowPresenter.STATE_SMALL);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- setupChildFragmentLayout();
- mStateMachine.fireEvent(EVT_ONSTART);
- if (mDetailsParallax != null) {
- mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
- }
- if (mPendingFocusOnVideo) {
- slideOutGridView();
- } else if (!getView().hasFocus()) {
- mRowsFragment.getVerticalGridView().requestFocus();
- }
- }
-
- @Override
- protected Object createEntranceTransition() {
- return TransitionHelper.loadTransition(FragmentUtil.getContext(DetailsFragment.this),
- R.transition.lb_details_enter_transition);
- }
-
- @Override
- protected void runEntranceTransition(Object entranceTransition) {
- TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
- }
-
- @Override
- protected void onEntranceTransitionEnd() {
- mRowsFragment.onTransitionEnd();
- }
-
- @Override
- protected void onEntranceTransitionPrepare() {
- mRowsFragment.onTransitionPrepare();
- }
-
- @Override
- protected void onEntranceTransitionStart() {
- mRowsFragment.onTransitionStart();
- }
-
- /**
- * Returns the {@link DetailsParallax} instance used by
- * {@link DetailsFragmentBackgroundController} to configure parallax effect of background and
- * control embedded video playback. App usually does not use this method directly.
- * App may use this method for other custom parallax tasks.
- *
- * @return The DetailsParallax instance attached to the DetailsFragment.
- */
- public DetailsParallax getParallax() {
- if (mDetailsParallax == null) {
- mDetailsParallax = new DetailsParallax();
- if (mRowsFragment != null && mRowsFragment.getView() != null) {
- mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
- }
- }
- return mDetailsParallax;
- }
-
- /**
- * Set background drawable shown below foreground rows UI and above
- * {@link #findOrCreateVideoFragment()}.
- *
- * @see DetailsFragmentBackgroundController
- */
- void setBackgroundDrawable(Drawable drawable) {
- if (mBackgroundView != null) {
- mBackgroundView.setBackground(drawable);
- }
- mBackgroundDrawable = drawable;
- }
-
- /**
- * This method does the following
- * <ul>
- * <li>sets up focus search handling logic in the root view to enable transitioning between
- * half screen/full screen/no video mode.</li>
- *
- * <li>Sets up the key listener in the root view to intercept events like UP/DOWN and
- * transition to appropriate mode like half/full screen video.</li>
- * </ul>
- */
- void setupDpadNavigation() {
- mRootView.setOnChildFocusListener(new BrowseFrameLayout.OnChildFocusListener() {
-
- @Override
- public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- return false;
- }
-
- @Override
- public void onRequestChildFocus(View child, View focused) {
- if (child != mRootView.getFocusedChild()) {
- if (child.getId() == R.id.details_fragment_root) {
- if (!mPendingFocusOnVideo) {
- slideInGridView();
- showTitle(true);
- }
- } else if (child.getId() == R.id.video_surface_container) {
- slideOutGridView();
- showTitle(false);
- } else {
- showTitle(true);
- }
- }
- }
- });
- mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
- @Override
- public View onFocusSearch(View focused, int direction) {
- if (mRowsFragment.getVerticalGridView() != null
- && mRowsFragment.getVerticalGridView().hasFocus()) {
- if (direction == View.FOCUS_UP) {
- if (mDetailsBackgroundController != null
- && mDetailsBackgroundController.canNavigateToVideoFragment()
- && mVideoFragment != null && mVideoFragment.getView() != null) {
- return mVideoFragment.getView();
- } else if (getTitleView() != null && getTitleView().hasFocusable()) {
- return getTitleView();
- }
- }
- } else if (getTitleView() != null && getTitleView().hasFocus()) {
- if (direction == View.FOCUS_DOWN) {
- if (mRowsFragment.getVerticalGridView() != null) {
- return mRowsFragment.getVerticalGridView();
- }
- }
- }
- return focused;
- }
- });
-
- // If we press BACK on remote while in full screen video mode, we should
- // transition back to half screen video playback mode.
- mRootView.setOnDispatchKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- // This is used to check if we are in full screen video mode. This is somewhat
- // hacky and relies on the behavior of the video helper class to update the
- // focusability of the video surface view.
- if (mVideoFragment != null && mVideoFragment.getView() != null
- && mVideoFragment.getView().hasFocus()) {
- if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
- if (getVerticalGridView().getChildCount() > 0) {
- getVerticalGridView().requestFocus();
- return true;
- }
- }
- }
-
- return false;
- }
- });
- }
-
- /**
- * Slides vertical grid view (displaying media item details) out of the screen from below.
- */
- void slideOutGridView() {
- if (getVerticalGridView() != null) {
- getVerticalGridView().animateOut();
- }
- }
-
- void slideInGridView() {
- if (getVerticalGridView() != null) {
- getVerticalGridView().animateIn();
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
deleted file mode 100644
index 223b8ef..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ /dev/null
@@ -1,495 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.animation.PropertyValuesHolder;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.widget.DetailsParallaxDrawable;
-import android.support.v17.leanback.widget.ParallaxTarget;
-import android.app.Fragment;
-
-/**
- * Controller for DetailsFragment parallax background and embedded video play.
- * <p>
- * The parallax background drawable is made of two parts: cover drawable (by default
- * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default
- * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size
- * of cover drawable and bottom drawable will be updated and the cover drawable will by default
- * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.
- * </p>
- * <pre>
- * ***************************
- * * Cover Drawable *
- * * (FitWidthBitmapDrawable)*
- * * *
- * ***************************
- * * DetailsOverviewRow *
- * * *
- * ***************************
- * * Bottom Drawable *
- * * (ColorDrawable) *
- * * Related *
- * * Content *
- * ***************************
- * </pre>
- * Both parallax background drawable and embedded video play are optional. App must call
- * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.
- * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and
- * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable
- * will be faded out.
- * Example:
- * <pre>
- * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(this);
- *
- * public void onCreate(Bundle savedInstance) {
- * super.onCreate(savedInstance);
- * MediaPlayerGlue player = new MediaPlayerGlue(..);
- * player.setUrl(...);
- * mController.enableParallax();
- * mController.setupVideoPlayback(player);
- * }
- *
- * static class MyLoadBitmapTask extends ... {
- * WeakReference<MyFragment> mFragmentRef;
- * MyLoadBitmapTask(MyFragment fragment) {
- * mFragmentRef = new WeakReference(fragment);
- * }
- * protected void onPostExecute(Bitmap bitmap) {
- * MyFragment fragment = mFragmentRef.get();
- * if (fragment != null) {
- * fragment.mController.setCoverBitmap(bitmap);
- * }
- * }
- * }
- *
- * public void onStart() {
- * new MyLoadBitmapTask(this).execute(url);
- * }
- *
- * public void onStop() {
- * mController.setCoverBitmap(null);
- * }
- * </pre>
- * <p>
- * To customize cover drawable and/or bottom drawable, app should call
- * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.
- * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.
- * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.
- * </p>
- * <p>
- * To customize playback fragment, app should override {@link #onCreateVideoFragment()} and
- * {@link #onCreateGlueHost()}.
- * </p>
- *
- */
-public class DetailsFragmentBackgroundController {
-
- final DetailsFragment mFragment;
- DetailsParallaxDrawable mParallaxDrawable;
- int mParallaxDrawableMaxOffset;
- PlaybackGlue mPlaybackGlue;
- DetailsBackgroundVideoHelper mVideoHelper;
- Bitmap mCoverBitmap;
- int mSolidColor;
- boolean mCanUseHost = false;
- boolean mInitialControlVisible = false;
-
- private Fragment mLastVideoFragmentForGlueHost;
-
- /**
- * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that
- * each DetailsFragment can only associate with one DetailsFragmentBackgroundController.
- *
- * @param fragment The DetailsFragment to control background and embedded video playing.
- * @throws IllegalStateException If fragment was already associated with another controller.
- */
- public DetailsFragmentBackgroundController(DetailsFragment fragment) {
- if (fragment.mDetailsBackgroundController != null) {
- throw new IllegalStateException("Each DetailsFragment is allowed to initialize "
- + "DetailsFragmentBackgroundController once");
- }
- fragment.mDetailsBackgroundController = this;
- mFragment = fragment;
- }
-
- /**
- * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable
- * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied
- * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and
- * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.
- * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
- *
- * @see #setCoverBitmap(Bitmap)
- * @see #setSolidColor(int)
- * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
- */
- public void enableParallax() {
- int offset = mParallaxDrawableMaxOffset;
- if (offset == 0) {
- offset = FragmentUtil.getContext(mFragment).getResources()
- .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);
- }
- Drawable coverDrawable = new FitWidthBitmapDrawable();
- ColorDrawable colorDrawable = new ColorDrawable();
- enableParallax(coverDrawable, colorDrawable,
- new ParallaxTarget.PropertyValuesHolderTarget(
- coverDrawable,
- PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
- 0, -offset)
- ));
- }
-
- /**
- * Enables parallax background using a custom cover drawable at top and a custom bottom
- * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.
- *
- * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}
- * will not work if coverDrawable is not {@link FitWidthBitmapDrawable};
- * in that case it's app's responsibility to set content into
- * coverDrawable.
- * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work
- * if bottomDrawable is not {@link ColorDrawable}; in that case it's app's
- * responsibility to set content of bottomDrawable.
- * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.
- * Use null for no parallax movement effect.
- * Example to move bitmap within FitWidthBitmapDrawable:
- * new ParallaxTarget.PropertyValuesHolderTarget(
- * coverDrawable, PropertyValuesHolder.ofInt(
- * FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,
- * 0, -120))
- * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.
- */
- public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,
- @Nullable ParallaxTarget.PropertyValuesHolderTarget
- coverDrawableParallaxTarget) {
- if (mParallaxDrawable != null) {
- return;
- }
- // if bitmap is set before enableParallax, use it as initial value.
- if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {
- ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);
- }
- // if solid color is set before enableParallax, use it as initial value.
- if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {
- ((ColorDrawable) bottomDrawable).setColor(mSolidColor);
- }
- if (mPlaybackGlue != null) {
- throw new IllegalStateException("enableParallaxDrawable must be called before "
- + "enableVideoPlayback");
- }
- mParallaxDrawable = new DetailsParallaxDrawable(
- FragmentUtil.getContext(mFragment),
- mFragment.getParallax(),
- coverDrawable,
- bottomDrawable,
- coverDrawableParallaxTarget);
- mFragment.setBackgroundDrawable(mParallaxDrawable);
- // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
- // before PlaybackGlue is ready.
- mVideoHelper = new DetailsBackgroundVideoHelper(null,
- mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
- }
-
- /**
- * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
- * creates a VideoFragment and VideoFragmentGlueHost to host the PlaybackGlue.
- * This method must be called after calling details Fragment super.onCreate(). This method
- * can be called multiple times to replace existing PlaybackGlue or calling
- * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases
- * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue}
- * subclass is not doing that, it's app's responsibility to release the resources.
- *
- * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one.
- * @see #onCreateVideoFragment()
- * @see #onCreateGlueHost().
- */
- @SuppressWarnings("ReferenceEquality")
- public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
- if (mPlaybackGlue == playbackGlue) {
- return;
- }
-
- PlaybackGlueHost playbackGlueHost = null;
- if (mPlaybackGlue != null) {
- playbackGlueHost = mPlaybackGlue.getHost();
- mPlaybackGlue.setHost(null);
- }
-
- mPlaybackGlue = playbackGlue;
- mVideoHelper.setPlaybackGlue(mPlaybackGlue);
- if (mCanUseHost && mPlaybackGlue != null) {
- if (playbackGlueHost == null
- || mLastVideoFragmentForGlueHost != findOrCreateVideoFragment()) {
- mPlaybackGlue.setHost(createGlueHost());
- mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
- } else {
- mPlaybackGlue.setHost(playbackGlueHost);
- }
- }
- }
-
- /**
- * Returns current PlaybackGlue or null if not set or cleared.
- *
- * @return Current PlaybackGlue or null
- */
- public final PlaybackGlue getPlaybackGlue() {
- return mPlaybackGlue;
- }
-
- /**
- * Precondition allows user navigate to video fragment using DPAD. Default implementation
- * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation
- * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block
- * app calls {@link #switchToVideo}.
- *
- * @return True allow to navigate to video fragment.
- */
- public boolean canNavigateToVideoFragment() {
- return mPlaybackGlue != null;
- }
-
- void switchToVideoBeforeCreate() {
- mVideoHelper.crossFadeBackgroundToVideo(true, true);
- mInitialControlVisible = true;
- }
-
- /**
- * Switch to video fragment, note that this method is not affected by result of
- * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()
- * it will make video fragment to be initially focused once it is created.
- * <p>
- * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter
- * transition and shared element transition.
- * </p>
- * <p>
- * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and
- * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.
- * </p>
- * <p>
- * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an
- * IllegalStateException will be thrown.
- * </p>
- */
- public final void switchToVideo() {
- mFragment.switchToVideo();
- }
-
- /**
- * Switch to rows fragment.
- */
- public final void switchToRows() {
- mFragment.switchToRows();
- }
-
- /**
- * When fragment is started and no running transition. First set host if not yet set, second
- * start playing if it was paused before.
- */
- void onStart() {
- if (!mCanUseHost) {
- mCanUseHost = true;
- if (mPlaybackGlue != null) {
- mPlaybackGlue.setHost(createGlueHost());
- mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();
- }
- }
- if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {
- mPlaybackGlue.play();
- }
- }
-
- void onStop() {
- if (mPlaybackGlue != null) {
- mPlaybackGlue.pause();
- }
- }
-
- /**
- * Disable parallax that would auto-start video playback
- * @return true if video fragment is visible or false otherwise.
- */
- boolean disableVideoParallax() {
- if (mVideoHelper != null) {
- mVideoHelper.stopParallax();
- return mVideoHelper.isVideoVisible();
- }
- return false;
- }
-
- /**
- * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.
- * By default it's a {@link FitWidthBitmapDrawable}.
- *
- * @return The cover drawable at top.
- */
- public final Drawable getCoverDrawable() {
- if (mParallaxDrawable == null) {
- return null;
- }
- return mParallaxDrawable.getCoverDrawable();
- }
-
- /**
- * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.
- * By default it's a {@link ColorDrawable}.
- *
- * @return The bottom drawable.
- */
- public final Drawable getBottomDrawable() {
- if (mParallaxDrawable == null) {
- return null;
- }
- return mParallaxDrawable.getBottomDrawable();
- }
-
- /**
- * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoFragment} by
- * default. App may override and return a different fragment and it also must override
- * {@link #onCreateGlueHost()}.
- *
- * @return A new fragment used in {@link #onCreateGlueHost()}.
- * @see #onCreateGlueHost()
- * @see #setupVideoPlayback(PlaybackGlue)
- */
- public Fragment onCreateVideoFragment() {
- return new VideoFragment();
- }
-
- /**
- * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides
- * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment
- * super.onCreate(). When override this method, app may call
- * {@link #findOrCreateVideoFragment()} to get or create a fragment.
- *
- * @return A new PlaybackGlueHost to host PlaybackGlue.
- * @see #onCreateVideoFragment()
- * @see #findOrCreateVideoFragment()
- * @see #setupVideoPlayback(PlaybackGlue)
- */
- public PlaybackGlueHost onCreateGlueHost() {
- return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());
- }
-
- PlaybackGlueHost createGlueHost() {
- PlaybackGlueHost host = onCreateGlueHost();
- if (mInitialControlVisible) {
- host.showControlsOverlay(false);
- } else {
- host.hideControlsOverlay(false);
- }
- return host;
- }
-
- /**
- * Adds or gets fragment for rendering video in DetailsFragment. A subclass that
- * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating
- * a {@link PlaybackGlueHost}.
- *
- * @return Fragment the added or restored fragment responsible for rendering video.
- * @see #onCreateGlueHost()
- */
- public final Fragment findOrCreateVideoFragment() {
- return mFragment.findOrCreateVideoFragment();
- }
-
- /**
- * Convenient method to set Bitmap in cover drawable. If app is not using default
- * {@link FitWidthBitmapDrawable}, app should not use this method It's safe to call
- * setCoverBitmap() before calling {@link #enableParallax()}.
- *
- * @param bitmap bitmap to set as cover.
- */
- public final void setCoverBitmap(Bitmap bitmap) {
- mCoverBitmap = bitmap;
- Drawable drawable = getCoverDrawable();
- if (drawable instanceof FitWidthBitmapDrawable) {
- ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);
- }
- }
-
- /**
- * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.
- *
- * @return Bitmap for cover drawable.
- */
- public final Bitmap getCoverBitmap() {
- return mCoverBitmap;
- }
-
- /**
- * Returns color set by {@link #setSolidColor(int)}.
- *
- * @return Solid color used for bottom drawable.
- */
- public final @ColorInt int getSolidColor() {
- return mSolidColor;
- }
-
- /**
- * Convenient method to set color in bottom drawable. If app is not using default
- * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()
- * before calling {@link #enableParallax()}.
- *
- * @param color color for bottom drawable.
- */
- public final void setSolidColor(@ColorInt int color) {
- mSolidColor = color;
- Drawable bottomDrawable = getBottomDrawable();
- if (bottomDrawable instanceof ColorDrawable) {
- ((ColorDrawable) bottomDrawable).setColor(color);
- }
- }
-
- /**
- * Sets default parallax offset in pixels for bitmap moving vertically. This method must
- * be called before {@link #enableParallax()}.
- *
- * @param offset Offset in pixels (e.g. 120).
- * @see #enableParallax()
- */
- public final void setParallaxDrawableMaxOffset(int offset) {
- if (mParallaxDrawable != null) {
- throw new IllegalStateException("enableParallax already called");
- }
- mParallaxDrawableMaxOffset = offset;
- }
-
- /**
- * Returns Default parallax offset in pixels for bitmap moving vertically.
- * When 0, a default value would be used.
- *
- * @return Default parallax offset in pixels for bitmap moving vertically.
- * @see #enableParallax()
- */
- public final int getParallaxDrawableMaxOffset() {
- return mParallaxDrawableMaxOffset;
- }
-
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java b/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
deleted file mode 100644
index 2896d0f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
+++ /dev/null
@@ -1,245 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from ErrorSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.graphics.Paint;
-import android.graphics.Paint.FontMetricsInt;
-import android.graphics.PixelFormat;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.v17.leanback.R;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * A fragment for displaying an error indication.
- */
-public class ErrorFragment extends BrandedFragment {
-
- private ViewGroup mErrorFrame;
- private ImageView mImageView;
- private TextView mTextView;
- private Button mButton;
- private Drawable mDrawable;
- private CharSequence mMessage;
- private String mButtonText;
- private View.OnClickListener mButtonClickListener;
- private Drawable mBackgroundDrawable;
- private boolean mIsBackgroundTranslucent = true;
-
- /**
- * Sets the default background.
- *
- * @param translucent True to set a translucent background.
- */
- public void setDefaultBackground(boolean translucent) {
- mBackgroundDrawable = null;
- mIsBackgroundTranslucent = translucent;
- updateBackground();
- updateMessage();
- }
-
- /**
- * Returns true if the background is translucent.
- */
- public boolean isBackgroundTranslucent() {
- return mIsBackgroundTranslucent;
- }
-
- /**
- * Sets a drawable for the fragment background.
- *
- * @param drawable The drawable used for the background.
- */
- public void setBackgroundDrawable(Drawable drawable) {
- mBackgroundDrawable = drawable;
- if (drawable != null) {
- final int opacity = drawable.getOpacity();
- mIsBackgroundTranslucent = (opacity == PixelFormat.TRANSLUCENT
- || opacity == PixelFormat.TRANSPARENT);
- }
- updateBackground();
- updateMessage();
- }
-
- /**
- * Returns the background drawable. May be null if a default is used.
- */
- public Drawable getBackgroundDrawable() {
- return mBackgroundDrawable;
- }
-
- /**
- * Sets the drawable to be used for the error image.
- *
- * @param drawable The drawable used for the error image.
- */
- public void setImageDrawable(Drawable drawable) {
- mDrawable = drawable;
- updateImageDrawable();
- }
-
- /**
- * Returns the drawable used for the error image.
- */
- public Drawable getImageDrawable() {
- return mDrawable;
- }
-
- /**
- * Sets the error message.
- *
- * @param message The error message.
- */
- public void setMessage(CharSequence message) {
- mMessage = message;
- updateMessage();
- }
-
- /**
- * Returns the error message.
- */
- public CharSequence getMessage() {
- return mMessage;
- }
-
- /**
- * Sets the button text.
- *
- * @param text The button text.
- */
- public void setButtonText(String text) {
- mButtonText = text;
- updateButton();
- }
-
- /**
- * Returns the button text.
- */
- public String getButtonText() {
- return mButtonText;
- }
-
- /**
- * Set the button click listener.
- *
- * @param clickListener The click listener for the button.
- */
- public void setButtonClickListener(View.OnClickListener clickListener) {
- mButtonClickListener = clickListener;
- updateButton();
- }
-
- /**
- * Returns the button click listener.
- */
- public View.OnClickListener getButtonClickListener() {
- return mButtonClickListener;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.lb_error_fragment, container, false);
-
- mErrorFrame = (ViewGroup) root.findViewById(R.id.error_frame);
- updateBackground();
-
- installTitleView(inflater, mErrorFrame, savedInstanceState);
-
- mImageView = (ImageView) root.findViewById(R.id.image);
- updateImageDrawable();
-
- mTextView = (TextView) root.findViewById(R.id.message);
- updateMessage();
-
- mButton = (Button) root.findViewById(R.id.button);
- updateButton();
-
- FontMetricsInt metrics = getFontMetricsInt(mTextView);
- int underImageBaselineMargin = container.getResources().getDimensionPixelSize(
- R.dimen.lb_error_under_image_baseline_margin);
- setTopMargin(mTextView, underImageBaselineMargin + metrics.ascent);
-
- int underMessageBaselineMargin = container.getResources().getDimensionPixelSize(
- R.dimen.lb_error_under_message_baseline_margin);
- setTopMargin(mButton, underMessageBaselineMargin - metrics.descent);
-
- return root;
- }
-
- private void updateBackground() {
- if (mErrorFrame != null) {
- if (mBackgroundDrawable != null) {
- mErrorFrame.setBackground(mBackgroundDrawable);
- } else {
- mErrorFrame.setBackgroundColor(mErrorFrame.getResources().getColor(
- mIsBackgroundTranslucent
- ? R.color.lb_error_background_color_translucent
- : R.color.lb_error_background_color_opaque));
- }
- }
- }
-
- private void updateMessage() {
- if (mTextView != null) {
- mTextView.setText(mMessage);
- mTextView.setVisibility(TextUtils.isEmpty(mMessage) ? View.GONE : View.VISIBLE);
- }
- }
-
- private void updateImageDrawable() {
- if (mImageView != null) {
- mImageView.setImageDrawable(mDrawable);
- mImageView.setVisibility(mDrawable == null ? View.GONE : View.VISIBLE);
- }
- }
-
- private void updateButton() {
- if (mButton != null) {
- mButton.setText(mButtonText);
- mButton.setOnClickListener(mButtonClickListener);
- mButton.setVisibility(TextUtils.isEmpty(mButtonText) ? View.GONE : View.VISIBLE);
- mButton.requestFocus();
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mErrorFrame.requestFocus();
- }
-
- private static FontMetricsInt getFontMetricsInt(TextView textView) {
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- paint.setTextSize(textView.getTextSize());
- paint.setTypeface(textView.getTypeface());
- return paint.getFontMetricsInt();
- }
-
- private static void setTopMargin(TextView textView, int topMargin) {
- ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
- lp.topMargin = topMargin;
- textView.setLayoutParams(lp);
- }
-
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
deleted file mode 100644
index 2b7f2d0..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
+++ /dev/null
@@ -1,1403 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * 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 android.support.v17.leanback.app;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionAdapter;
-import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
-import android.support.v4.app.ActivityCompat;
-import android.app.Fragment;
-import android.app.Activity;
-import android.app.FragmentManager;
-import android.app.FragmentManager.BackStackEntry;
-import android.app.FragmentTransaction;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A GuidedStepFragment is used to guide the user through a decision or series of decisions.
- * It is composed of a guidance view on the left and a view on the right containing a list of
- * possible actions.
- * <p>
- * <h3>Basic Usage</h3>
- * <p>
- * Clients of GuidedStepFragment must create a custom subclass to attach to their Activities.
- * This custom subclass provides the information necessary to construct the user interface and
- * respond to user actions. At a minimum, subclasses should override:
- * <ul>
- * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
- * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
- * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
- * </ul>
- * <p>
- * Clients use following helper functions to add GuidedStepFragment to Activity or FragmentManager:
- * <ul>
- * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)}, to be called during Activity onCreate,
- * adds GuidedStepFragment as the first Fragment in activity.</li>
- * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
- * GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or
- * replacing existing GuidedStepFragment when moving forward to next step.</li>
- * <li>{@link #finishGuidedStepFragments()} can either finish the activity or pop all
- * GuidedStepFragment from stack.
- * <li>If app chooses not to use the helper function, it is the app's responsibility to call
- * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
- * need pops to.
- * </ul>
- * <h3>Theming and Stylists</h3>
- * <p>
- * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link
- * GuidanceStylist} is responsible for the left guidance view, while the {@link
- * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
- * attributes to derive values associated with the presentation, such as colors, animations, etc.
- * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
- * via theming; see their documentation for more information.
- * <p>
- * GuidedStepFragments must have access to an appropriate theme in order for the stylists to
- * function properly. Specifically, the fragment must receive {@link
- * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
- * is set to that theme. Themes can be provided in one of three ways:
- * <ul>
- * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
- * theme that derives from it.</li>
- * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
- * existing Activity theme can have an entry added for the attribute {@link
- * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
- * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li>
- * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link
- * #onProvideTheme} method. This can be useful if a subclass is used across multiple
- * Activities.</li>
- * </ul>
- * <p>
- * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
- * the Activity's theme. (Themes whose parent theme is already set to the guided step theme do not
- * need to set the guidedStepTheme attribute; if set, it will be ignored.)
- * <p>
- * If themes do not provide enough customizability, the stylists themselves may be subclassed and
- * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link
- * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses
- * may override layout files; subclasses may also have more complex logic to determine styling.
- * <p>
- * <h3>Guided sequences</h3>
- * <p>
- * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments
- * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
- * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
- * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
- * custom animations are properly configured. (Custom animations are triggered automatically when
- * the fragment stack is subsequently popped by any normal mechanism.)
- * <p>
- * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically,
- * rather than in XML. This restriction may be removed in the future.</i>
- *
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation
- * @see GuidanceStylist
- * @see GuidanceStylist.Guidance
- * @see GuidedAction
- * @see GuidedActionsStylist
- */
-public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener {
-
- private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment";
- private static final String EXTRA_ACTION_PREFIX = "action_";
- private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_";
-
- private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault";
-
- private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
-
- private static final boolean IS_FRAMEWORK_FRAGMENT = true;
-
- /**
- * Fragment argument name for UI style. The argument value is persisted in fragment state and
- * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and
- * might be changed in one of the three helper functions:
- * <ul>
- * <li>{@link #addAsRoot(Activity, GuidedStepFragment, int)} sets to
- * {@link #UI_STYLE_ACTIVITY_ROOT}</li>
- * <li>{@link #add(FragmentManager, GuidedStepFragment)} or {@link #add(FragmentManager,
- * GuidedStepFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a
- * GuidedStepFragment on stack.</li>
- * <li>{@link #finishGuidedStepFragments()} changes current GuidedStepFragment to
- * {@link #UI_STYLE_ENTRANCE} for the non activity case. This is a special case that changes
- * the transition settings after fragment has been created, in order to force current
- * GuidedStepFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li>
- * </ul>
- * <p>
- * Argument value can be either:
- * <ul>
- * <li>{@link #UI_STYLE_REPLACE}</li>
- * <li>{@link #UI_STYLE_ENTRANCE}</li>
- * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
- * </ul>
- */
- public static final String EXTRA_UI_STYLE = "uiStyle";
-
- /**
- * This is the case that we use GuidedStepFragment to replace another existing
- * GuidedStepFragment when moving forward to next step. Default behavior of this style is:
- * <ul>
- * <li>Enter transition slides in from END(right), exit transition same as
- * {@link #UI_STYLE_ENTRANCE}.
- * </li>
- * </ul>
- */
- public static final int UI_STYLE_REPLACE = 0;
-
- /**
- * @deprecated Same value as {@link #UI_STYLE_REPLACE}.
- */
- @Deprecated
- public static final int UI_STYLE_DEFAULT = 0;
-
- /**
- * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in
- * GuidedStepFragment constructor. This is the case that we show GuidedStepFragment on top of
- * other content. The default behavior of this style:
- * <ul>
- * <li>Enter transition slides in from two sides, exit transition slide out to START(left).
- * Background will be faded in. Note: Changing exit transition by UI style is not working
- * because fragment transition asks for exit transition before UI style is restored in Fragment
- * .onCreate().</li>
- * </ul>
- * When popping multiple GuidedStepFragment, {@link #finishGuidedStepFragments()} also changes
- * the top GuidedStepFragment to UI_STYLE_ENTRANCE in order to run the return transition
- * (reverse of enter transition) of UI_STYLE_ENTRANCE.
- */
- public static final int UI_STYLE_ENTRANCE = 1;
-
- /**
- * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
- * GuidedStepFragment in a separate activity. The default behavior of this style:
- * <ul>
- * <li>Enter transition is assigned null (will rely on activity transition), exit transition is
- * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working
- * because fragment transition asks for exit transition before UI style is restored in
- * Fragment.onCreate().</li>
- * </ul>
- */
- public static final int UI_STYLE_ACTIVITY_ROOT = 2;
-
- /**
- * Animation to slide the contents from the side (left/right).
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final int SLIDE_FROM_SIDE = 0;
-
- /**
- * Animation to slide the contents from the bottom.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final int SLIDE_FROM_BOTTOM = 1;
-
- private static final String TAG = "GuidedStepF";
- private static final boolean DEBUG = false;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static class DummyFragment extends Fragment {
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- final View v = new View(inflater.getContext());
- v.setVisibility(View.GONE);
- return v;
- }
- }
-
- private ContextThemeWrapper mThemeWrapper;
- private GuidanceStylist mGuidanceStylist;
- GuidedActionsStylist mActionsStylist;
- private GuidedActionsStylist mButtonActionsStylist;
- private GuidedActionAdapter mAdapter;
- private GuidedActionAdapter mSubAdapter;
- private GuidedActionAdapter mButtonAdapter;
- private GuidedActionAdapterGroup mAdapterGroup;
- private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
- private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
- private int entranceTransitionType = SLIDE_FROM_SIDE;
-
- public GuidedStepFragment() {
- mGuidanceStylist = onCreateGuidanceStylist();
- mActionsStylist = onCreateActionsStylist();
- mButtonActionsStylist = onCreateButtonActionsStylist();
- onProvideFragmentTransitions();
- }
-
- /**
- * Creates the presenter used to style the guidance panel. The default implementation returns
- * a basic GuidanceStylist.
- * @return The GuidanceStylist used in this fragment.
- */
- public GuidanceStylist onCreateGuidanceStylist() {
- return new GuidanceStylist();
- }
-
- /**
- * Creates the presenter used to style the guided actions panel. The default implementation
- * returns a basic GuidedActionsStylist.
- * @return The GuidedActionsStylist used in this fragment.
- */
- public GuidedActionsStylist onCreateActionsStylist() {
- return new GuidedActionsStylist();
- }
-
- /**
- * Creates the presenter used to style a sided actions panel for button only.
- * The default implementation returns a basic GuidedActionsStylist.
- * @return The GuidedActionsStylist used in this fragment.
- */
- public GuidedActionsStylist onCreateButtonActionsStylist() {
- GuidedActionsStylist stylist = new GuidedActionsStylist();
- stylist.setAsButtonActions();
- return stylist;
- }
-
- /**
- * Returns the theme used for styling the fragment. The default returns -1, indicating that the
- * host Activity's theme should be used.
- * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
- * host Activity's theme.
- */
- public int onProvideTheme() {
- return -1;
- }
-
- /**
- * Returns the information required to provide guidance to the user. This hook is called during
- * {@link #onCreateView}. May be overridden to return a custom subclass of {@link
- * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
- * returns a Guidance object with empty fields; subclasses should override.
- * @param savedInstanceState The saved instance state from onCreateView.
- * @return The Guidance object representing the information used to guide the user.
- */
- public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
- return new Guidance("", "", "", null);
- }
-
- /**
- * Fills out the set of actions available to the user. This hook is called during {@link
- * #onCreate}. The default leaves the list of actions empty; subclasses should override.
- * @param actions A non-null, empty list ready to be populated.
- * @param savedInstanceState The saved instance state from onCreate.
- */
- public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- /**
- * Fills out the set of actions shown at right available to the user. This hook is called during
- * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
- * @param actions A non-null, empty list ready to be populated.
- * @param savedInstanceState The saved instance state from onCreate.
- */
- public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
- Bundle savedInstanceState) {
- }
-
- /**
- * Callback invoked when an action is taken by the user. Subclasses should override in
- * order to act on the user's decisions.
- * @param action The chosen action.
- */
- public void onGuidedActionClicked(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action in sub actions is taken by the user. Subclasses should
- * override in order to act on the user's decisions. Default return value is true to close
- * the sub actions list.
- * @param action The chosen action.
- * @return true to collapse the sub actions list, false to keep it expanded.
- */
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return true;
- }
-
- /**
- * @return True if is current expanded including subactions list or
- * action with {@link GuidedAction#hasEditableActivatorView()} is true.
- */
- public boolean isExpanded() {
- return mActionsStylist.isExpanded();
- }
-
- /**
- * @return True if the sub actions list is expanded, false otherwise.
- */
- public boolean isSubActionsExpanded() {
- return mActionsStylist.isSubActionsExpanded();
- }
-
- /**
- * Expand a given action's sub actions list.
- * @param action GuidedAction to expand.
- * @see #expandAction(GuidedAction, boolean)
- */
- public void expandSubActions(GuidedAction action) {
- if (!action.hasSubActions()) {
- return;
- }
- expandAction(action, true);
- }
-
- /**
- * Expand a given action with sub actions list or
- * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after
- * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view.
- *
- * @param action GuidedAction to expand.
- * @param withTransition True to run transition animation, false otherwise.
- */
- public void expandAction(GuidedAction action, boolean withTransition) {
- mActionsStylist.expandAction(action, withTransition);
- }
-
- /**
- * Collapse sub actions list.
- * @see GuidedAction#getSubActions()
- */
- public void collapseSubActions() {
- collapseAction(true);
- }
-
- /**
- * Collapse action which either has a sub actions list or action with
- * {@link GuidedAction#hasEditableActivatorView()} is true.
- *
- * @param withTransition True to run transition animation, false otherwise.
- */
- public void collapseAction(boolean withTransition) {
- if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) {
- mActionsStylist.collapseAction(withTransition);
- }
- }
-
- /**
- * Callback invoked when an action is focused (made to be the current selection) by the user.
- */
- @Override
- public void onGuidedActionFocused(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action's title or description has been edited, this happens either
- * when user clicks confirm button in IME or user closes IME window by BACK key.
- * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or
- * {@link #onGuidedActionEditCanceled(GuidedAction)}.
- */
- @Deprecated
- public void onGuidedActionEdited(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action has been canceled editing, for example when user closes
- * IME window by BACK key. Default implementation calls deprecated method
- * {@link #onGuidedActionEdited(GuidedAction)}.
- * @param action The action which has been canceled editing.
- */
- public void onGuidedActionEditCanceled(GuidedAction action) {
- onGuidedActionEdited(action);
- }
-
- /**
- * Callback invoked when an action has been edited, for example when user clicks confirm button
- * in IME window. Default implementation calls deprecated method
- * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}.
- *
- * @param action The action that has been edited.
- * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
- * {@link GuidedAction#ACTION_ID_CURRENT}.
- */
- public long onGuidedActionEditedAndProceed(GuidedAction action) {
- onGuidedActionEdited(action);
- return GuidedAction.ACTION_ID_NEXT;
- }
-
- /**
- * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
- * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
- * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
- * is pressed.
- * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE}
- * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
- * <p>
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param fragmentManager The FragmentManager to be used in the transaction.
- * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
- * @return The ID returned by the call FragmentTransaction.commit.
- */
- public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) {
- return add(fragmentManager, fragment, android.R.id.content);
- }
-
- /**
- * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
- * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
- * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
- * is pressed.
- * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} and
- * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepFragment)} will be called
- * to perform shared element transition between GuidedStepFragments.
- * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
- * <p>
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param fragmentManager The FragmentManager to be used in the transaction.
- * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
- * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
- * @return The ID returned by the call FragmentTransaction.commit.
- */
- public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment, int id) {
- GuidedStepFragment current = getCurrentGuidedStepFragment(fragmentManager);
- boolean inGuidedStep = current != null;
- if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
- && !inGuidedStep) {
- // workaround b/22631964 for framework fragment
- fragmentManager.beginTransaction()
- .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
- .commit();
- }
- FragmentTransaction ft = fragmentManager.beginTransaction();
-
- fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
- ft.addToBackStack(fragment.generateStackEntryName());
- if (current != null) {
- fragment.onAddSharedElementTransition(ft, current);
- }
- return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
- }
-
- /**
- * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
- * when the GuidedStepFragment replacing an existing GuidedStepFragment). Default implementation
- * establishes connections between action background views to morph action background bounds
- * change from disappearing GuidedStepFragment into this GuidedStepFragment. The default
- * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
- * method when modifying the default layout of {@link GuidedActionsStylist}.
- *
- * @see GuidedActionsStylist
- * @see #onProvideFragmentTransitions()
- * @param ft The FragmentTransaction to add shared element.
- * @param disappearing The disappearing fragment.
- */
- protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepFragment
- disappearing) {
- View fragmentView = disappearing.getView();
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment_root), "action_fragment_root");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment_background), "action_fragment_background");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment), "action_fragment");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_root), "guidedactions_root");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_content), "guidedactions_content");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_list_background), "guidedactions_list_background");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_root2), "guidedactions_root2");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_content2), "guidedactions_content2");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_list_background2), "guidedactions_list_background2");
- }
-
- private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
- String transitionName)
- {
- if (subView != null)
- TransitionHelper.addSharedElement(ft, subView, transitionName);
- }
-
- /**
- * Returns BackStackEntry name for the GuidedStepFragment or empty String if no entry is
- * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method
- * returns undefined value if the fragment is not in FragmentManager.
- * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
- * associated.
- */
- final String generateStackEntryName() {
- return generateStackEntryName(getUiStyle(), getClass());
- }
-
- /**
- * Generates BackStackEntry name for GuidedStepFragment class or empty String if no entry is
- * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
- * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
- * @return BackStackEntry name for the GuidedStepFragment or empty String if no entry is
- * associated.
- */
- static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
- switch (uiStyle) {
- case UI_STYLE_REPLACE:
- return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
- case UI_STYLE_ENTRANCE:
- return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
- case UI_STYLE_ACTIVITY_ROOT:
- default:
- return "";
- }
- }
-
- /**
- * Returns true if the backstack entry represents GuidedStepFragment with
- * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepFragment pushed to stack; false
- * otherwise.
- * @see #generateStackEntryName(int, Class)
- * @param backStackEntryName Name of BackStackEntry.
- * @return True if the backstack represents GuidedStepFragment with {@link #UI_STYLE_ENTRANCE};
- * false otherwise.
- */
- static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
- return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
- }
-
- /**
- * Extract Class name from BackStackEntry name.
- * @param backStackEntryName Name of BackStackEntry.
- * @return Class name of GuidedStepFragment.
- */
- static String getGuidedStepFragmentClassName(String backStackEntryName) {
- if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
- return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
- } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
- return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
- } else {
- return "";
- }
- }
-
- /**
- * Adds the specified GuidedStepFragment as content of Activity; no backstack entry is added so
- * the activity will be dismissed when BACK key is pressed. The method is typically called in
- * Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null,
- * the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored
- * by FragmentManager.
- * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
- *
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param activity The Activity to be used to insert GuidedstepFragment.
- * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
- * @param id The id of container to add GuidedStepFragment, can be android.R.id.content.
- * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
- * GuidedStepFragment.
- */
- public static int addAsRoot(Activity activity, GuidedStepFragment fragment, int id) {
- // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
- activity.getWindow().getDecorView();
- FragmentManager fragmentManager = activity.getFragmentManager();
- if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
- Log.w(TAG, "Fragment is already exists, likely calling "
- + "addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
- return -1;
- }
- FragmentTransaction ft = fragmentManager.beginTransaction();
- fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
- return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
- }
-
- /**
- * Returns the current GuidedStepFragment on the fragment transaction stack.
- * @return The current GuidedStepFragment, if any, on the fragment transaction stack.
- */
- public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) {
- Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
- if (f instanceof GuidedStepFragment) {
- return (GuidedStepFragment) f;
- }
- return null;
- }
-
- /**
- * Returns the GuidanceStylist that displays guidance information for the user.
- * @return The GuidanceStylist for this fragment.
- */
- public GuidanceStylist getGuidanceStylist() {
- return mGuidanceStylist;
- }
-
- /**
- * Returns the GuidedActionsStylist that displays the actions the user may take.
- * @return The GuidedActionsStylist for this fragment.
- */
- public GuidedActionsStylist getGuidedActionsStylist() {
- return mActionsStylist;
- }
-
- /**
- * Returns the list of button GuidedActions that the user may take in this fragment.
- * @return The list of button GuidedActions for this fragment.
- */
- public List<GuidedAction> getButtonActions() {
- return mButtonActions;
- }
-
- /**
- * Find button GuidedAction by Id.
- * @param id Id of the button action to search.
- * @return GuidedAction object or null if not found.
- */
- public GuidedAction findButtonActionById(long id) {
- int index = findButtonActionPositionById(id);
- return index >= 0 ? mButtonActions.get(index) : null;
- }
-
- /**
- * Find button GuidedAction position in array by Id.
- * @param id Id of the button action to search.
- * @return position of GuidedAction object in array or -1 if not found.
- */
- public int findButtonActionPositionById(long id) {
- if (mButtonActions != null) {
- for (int i = 0; i < mButtonActions.size(); i++) {
- GuidedAction action = mButtonActions.get(i);
- if (mButtonActions.get(i).getId() == id) {
- return i;
- }
- }
- }
- return -1;
- }
-
- /**
- * Returns the GuidedActionsStylist that displays the button actions the user may take.
- * @return The GuidedActionsStylist for this fragment.
- */
- public GuidedActionsStylist getGuidedButtonActionsStylist() {
- return mButtonActionsStylist;
- }
-
- /**
- * Sets the list of button GuidedActions that the user may take in this fragment.
- * @param actions The list of button GuidedActions for this fragment.
- */
- public void setButtonActions(List<GuidedAction> actions) {
- mButtonActions = actions;
- if (mButtonAdapter != null) {
- mButtonAdapter.setActions(mButtonActions);
- }
- }
-
- /**
- * Notify an button action has changed and update its UI.
- * @param position Position of the button GuidedAction in array.
- */
- public void notifyButtonActionChanged(int position) {
- if (mButtonAdapter != null) {
- mButtonAdapter.notifyItemChanged(position);
- }
- }
-
- /**
- * Returns the view corresponding to the button action at the indicated position in the list of
- * actions for this fragment.
- * @param position The integer position of the button action of interest.
- * @return The View corresponding to the button action at the indicated position, or null if
- * that action is not currently onscreen.
- */
- public View getButtonActionItemView(int position) {
- final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
- .findViewHolderForPosition(position);
- return holder == null ? null : holder.itemView;
- }
-
- /**
- * Scrolls the action list to the position indicated, selecting that button action's view.
- * @param position The integer position of the button action of interest.
- */
- public void setSelectedButtonActionPosition(int position) {
- mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
- }
-
- /**
- * Returns the position if the currently selected button GuidedAction.
- * @return position The integer position of the currently selected button action.
- */
- public int getSelectedButtonActionPosition() {
- return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
- }
-
- /**
- * Returns the list of GuidedActions that the user may take in this fragment.
- * @return The list of GuidedActions for this fragment.
- */
- public List<GuidedAction> getActions() {
- return mActions;
- }
-
- /**
- * Find GuidedAction by Id.
- * @param id Id of the action to search.
- * @return GuidedAction object or null if not found.
- */
- public GuidedAction findActionById(long id) {
- int index = findActionPositionById(id);
- return index >= 0 ? mActions.get(index) : null;
- }
-
- /**
- * Find GuidedAction position in array by Id.
- * @param id Id of the action to search.
- * @return position of GuidedAction object in array or -1 if not found.
- */
- public int findActionPositionById(long id) {
- if (mActions != null) {
- for (int i = 0; i < mActions.size(); i++) {
- GuidedAction action = mActions.get(i);
- if (mActions.get(i).getId() == id) {
- return i;
- }
- }
- }
- return -1;
- }
-
- /**
- * Sets the list of GuidedActions that the user may take in this fragment.
- * @param actions The list of GuidedActions for this fragment.
- */
- public void setActions(List<GuidedAction> actions) {
- mActions = actions;
- if (mAdapter != null) {
- mAdapter.setActions(mActions);
- }
- }
-
- /**
- * Notify an action has changed and update its UI.
- * @param position Position of the GuidedAction in array.
- */
- public void notifyActionChanged(int position) {
- if (mAdapter != null) {
- mAdapter.notifyItemChanged(position);
- }
- }
-
- /**
- * Returns the view corresponding to the action at the indicated position in the list of
- * actions for this fragment.
- * @param position The integer position of the action of interest.
- * @return The View corresponding to the action at the indicated position, or null if that
- * action is not currently onscreen.
- */
- public View getActionItemView(int position) {
- final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
- .findViewHolderForPosition(position);
- return holder == null ? null : holder.itemView;
- }
-
- /**
- * Scrolls the action list to the position indicated, selecting that action's view.
- * @param position The integer position of the action of interest.
- */
- public void setSelectedActionPosition(int position) {
- mActionsStylist.getActionsGridView().setSelectedPosition(position);
- }
-
- /**
- * Returns the position if the currently selected GuidedAction.
- * @return position The integer position of the currently selected action.
- */
- public int getSelectedActionPosition() {
- return mActionsStylist.getActionsGridView().getSelectedPosition();
- }
-
- /**
- * Called by Constructor to provide fragment transitions. The default implementation assigns
- * transitions based on {@link #getUiStyle()}:
- * <ul>
- * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
- * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
- * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
- * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
- * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
- * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
- * enter transition.
- * </ul>
- * <p>
- * The default implementation heavily relies on {@link GuidedActionsStylist} and
- * {@link GuidanceStylist} layout, app may override this method when modifying the default
- * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
- * <p>
- * TIP: because the fragment view is removed during fragment transition, in general app cannot
- * use two Visibility transition together. Workaround is to create your own Visibility
- * transition that controls multiple animators (e.g. slide and fade animation in one Transition
- * class).
- */
- protected void onProvideFragmentTransitions() {
- if (Build.VERSION.SDK_INT >= 21) {
- final int uiStyle = getUiStyle();
- if (uiStyle == UI_STYLE_REPLACE) {
- Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
- TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
- TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
- true);
- TransitionHelper.setEnterTransition(this, enterTransition);
-
- Object fade = TransitionHelper.createFadeTransition(
- TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
- TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
- Object changeBounds = TransitionHelper.createChangeBounds(false);
- Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(sharedElementTransition, fade);
- TransitionHelper.addTransition(sharedElementTransition, changeBounds);
- TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
- } else if (uiStyle == UI_STYLE_ENTRANCE) {
- if (entranceTransitionType == SLIDE_FROM_SIDE) {
- Object fade = TransitionHelper.createFadeTransition(
- TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
- TransitionHelper.include(fade, R.id.guidedstep_background);
- Object slideFromSide = TransitionHelper.createFadeAndShortSlide(
- Gravity.END | Gravity.START);
- TransitionHelper.include(slideFromSide, R.id.content_fragment);
- TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
- Object enterTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(enterTransition, fade);
- TransitionHelper.addTransition(enterTransition, slideFromSide);
- TransitionHelper.setEnterTransition(this, enterTransition);
- } else {
- Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
- Gravity.BOTTOM);
- TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
- Object enterTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(enterTransition, slideFromBottom);
- TransitionHelper.setEnterTransition(this, enterTransition);
- }
- // No shared element transition
- TransitionHelper.setSharedElementEnterTransition(this, null);
- } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
- // for Activity root, we don't need enter transition, use activity transition
- TransitionHelper.setEnterTransition(this, null);
- // No shared element transition
- TransitionHelper.setSharedElementEnterTransition(this, null);
- }
- // exitTransition is same for all style
- Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
- TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
- TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
- true);
- TransitionHelper.setExitTransition(this, exitTransition);
- }
- }
-
- /**
- * Called by onCreateView to inflate background view. Default implementation loads view
- * from {@link R.layout#lb_guidedstep_background} which holds a reference to
- * guidedStepBackground.
- * @param inflater LayoutInflater to load background view.
- * @param container Parent view of background view.
- * @param savedInstanceState
- * @return Created background view or null if no background.
- */
- public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
- }
-
- /**
- * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
- * is first initialized. UI style is used to choose different fragment transition animations and
- * determine if this is the first GuidedStepFragment on backstack. In most cases app does not
- * directly call this method, app calls helper function
- * {@link #add(FragmentManager, GuidedStepFragment, int)}. However if the app creates Fragment
- * transaction and controls backstack by itself, it would need call setUiStyle() to select the
- * fragment transition to use.
- *
- * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
- * {@link #UI_STYLE_ENTRANCE}.
- */
- public void setUiStyle(int style) {
- int oldStyle = getUiStyle();
- Bundle arguments = getArguments();
- boolean isNew = false;
- if (arguments == null) {
- arguments = new Bundle();
- isNew = true;
- }
- arguments.putInt(EXTRA_UI_STYLE, style);
- // call setArgument() will validate if the fragment is already added.
- if (isNew) {
- setArguments(arguments);
- }
- if (style != oldStyle) {
- onProvideFragmentTransitions();
- }
- }
-
- /**
- * Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when
- * fragment is first initialized. UI style is used to choose different fragment transition
- * animations and determine if this is the first GuidedStepFragment on backstack.
- *
- * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
- * {@link #UI_STYLE_ENTRANCE}.
- * @see #onProvideFragmentTransitions()
- */
- public int getUiStyle() {
- Bundle b = getArguments();
- if (b == null) return UI_STYLE_ENTRANCE;
- return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (DEBUG) Log.v(TAG, "onCreate");
- // Set correct transition from saved arguments.
- onProvideFragmentTransitions();
-
- ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
- onCreateActions(actions, savedInstanceState);
- if (savedInstanceState != null) {
- onRestoreActions(actions, savedInstanceState);
- }
- setActions(actions);
- ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
- onCreateButtonActions(buttonActions, savedInstanceState);
- if (savedInstanceState != null) {
- onRestoreButtonActions(buttonActions, savedInstanceState);
- }
- setButtonActions(buttonActions);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onDestroyView() {
- mGuidanceStylist.onDestroyView();
- mActionsStylist.onDestroyView();
- mButtonActionsStylist.onDestroyView();
- mAdapter = null;
- mSubAdapter = null;
- mButtonAdapter = null;
- mAdapterGroup = null;
- super.onDestroyView();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- if (DEBUG) Log.v(TAG, "onCreateView");
-
- resolveTheme();
- inflater = getThemeInflater(inflater);
-
- GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate(
- R.layout.lb_guidedstep_fragment, container, false);
-
- root.setFocusOutStart(isFocusOutStartAllowed());
- root.setFocusOutEnd(isFocusOutEndAllowed());
-
- ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
- ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
- ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
-
- Guidance guidance = onCreateGuidance(savedInstanceState);
- View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
- guidanceContainer.addView(guidanceView);
-
- View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
- actionContainer.addView(actionsView);
-
- View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
- actionContainer.addView(buttonActionsView);
-
- GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
-
- @Override
- public void onImeOpen() {
- runImeAnimations(true);
- }
-
- @Override
- public void onImeClose() {
- runImeAnimations(false);
- }
-
- @Override
- public long onGuidedActionEditedAndProceed(GuidedAction action) {
- return GuidedStepFragment.this.onGuidedActionEditedAndProceed(action);
- }
-
- @Override
- public void onGuidedActionEditCanceled(GuidedAction action) {
- GuidedStepFragment.this.onGuidedActionEditCanceled(action);
- }
- };
-
- mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- GuidedStepFragment.this.onGuidedActionClicked(action);
- if (isExpanded()) {
- collapseAction(true);
- } else if (action.hasSubActions() || action.hasEditableActivatorView()) {
- expandAction(action, true);
- }
- }
- }, this, mActionsStylist, false);
- mButtonAdapter =
- new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- GuidedStepFragment.this.onGuidedActionClicked(action);
- }
- }, this, mButtonActionsStylist, false);
- mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (mActionsStylist.isInExpandTransition()) {
- return;
- }
- if (GuidedStepFragment.this.onSubGuidedActionClicked(action)) {
- collapseSubActions();
- }
- }
- }, this, mActionsStylist, true);
- mAdapterGroup = new GuidedActionAdapterGroup();
- mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
- mAdapterGroup.addAdpter(mSubAdapter, null);
- mAdapterGroup.setEditListener(editListener);
- mActionsStylist.setEditListener(editListener);
-
- mActionsStylist.getActionsGridView().setAdapter(mAdapter);
- if (mActionsStylist.getSubActionsGridView() != null) {
- mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter);
- }
- mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
- if (mButtonActions.size() == 0) {
- // when there is no button actions, we don't need show the second panel, but keep
- // the width zero to run ChangeBounds transition.
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
- buttonActionsView.getLayoutParams();
- lp.weight = 0;
- buttonActionsView.setLayoutParams(lp);
- } else {
- // when there are two actions panel, we need adjust the weight of action to
- // guidedActionContentWidthWeightTwoPanels.
- Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(GuidedStepFragment.this);
- TypedValue typedValue = new TypedValue();
- if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
- typedValue, true)) {
- View actionsRoot = root.findViewById(R.id.action_fragment_root);
- float weight = typedValue.getFloat();
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
- .getLayoutParams();
- lp.weight = weight;
- actionsRoot.setLayoutParams(lp);
- }
- }
-
- // Add the background view.
- View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState);
- if (backgroundView != null) {
- FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById(
- R.id.guidedstep_background_view_root);
- backgroundViewRoot.addView(backgroundView, 0);
- }
-
- return root;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- getView().findViewById(R.id.action_fragment).requestFocus();
- }
-
- /**
- * Get the key will be used to save GuidedAction with Fragment.
- * @param action GuidedAction to get key.
- * @return Key to save the GuidedAction.
- */
- final String getAutoRestoreKey(GuidedAction action) {
- return EXTRA_ACTION_PREFIX + action.getId();
- }
-
- /**
- * Get the key will be used to save GuidedAction with Fragment.
- * @param action GuidedAction to get key.
- * @return Key to save the GuidedAction.
- */
- final String getButtonAutoRestoreKey(GuidedAction action) {
- return EXTRA_BUTTON_ACTION_PREFIX + action.getId();
- }
-
- final static boolean isSaveEnabled(GuidedAction action) {
- return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID;
- }
-
- final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action));
- }
- }
- }
-
- final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action));
- }
- }
- }
-
- final void onSaveActions(List<GuidedAction> actions, Bundle outState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onSaveInstanceState(outState, getAutoRestoreKey(action));
- }
- }
- }
-
- final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action));
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- onSaveActions(mActions, outState);
- onSaveButtonActions(mButtonActions, outState);
- }
-
- private static boolean isGuidedStepTheme(Context context) {
- int resId = R.attr.guidedStepThemeFlag;
- TypedValue typedValue = new TypedValue();
- boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
- if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
- return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
- }
-
- /**
- * Convenient method to close GuidedStepFragments on top of other content or finish Activity if
- * GuidedStepFragments were started in a separate activity. Pops all stack entries including
- * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
- * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepFragment,
- * int)} which sets up the stack entry name for finding which fragment we need to pop back to.
- */
- public void finishGuidedStepFragments() {
- final FragmentManager fragmentManager = getFragmentManager();
- final int entryCount = fragmentManager.getBackStackEntryCount();
- if (entryCount > 0) {
- for (int i = entryCount - 1; i >= 0; i--) {
- BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
- if (isStackEntryUiStyleEntrance(entry.getName())) {
- GuidedStepFragment top = getCurrentGuidedStepFragment(fragmentManager);
- if (top != null) {
- top.setUiStyle(UI_STYLE_ENTRANCE);
- }
- fragmentManager.popBackStackImmediate(entry.getId(),
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- return;
- }
- }
- }
- ActivityCompat.finishAfterTransition(getActivity());
- }
-
- /**
- * Convenient method to pop to fragment with Given class.
- * @param guidedStepFragmentClass Name of the Class of GuidedStepFragment to pop to.
- * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
- */
- public void popBackStackToGuidedStepFragment(Class guidedStepFragmentClass, int flags) {
- if (!GuidedStepFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
- return;
- }
- final FragmentManager fragmentManager = getFragmentManager();
- final int entryCount = fragmentManager.getBackStackEntryCount();
- String className = guidedStepFragmentClass.getName();
- if (entryCount > 0) {
- for (int i = entryCount - 1; i >= 0; i--) {
- BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
- String entryClassName = getGuidedStepFragmentClassName(entry.getName());
- if (className.equals(entryClassName)) {
- fragmentManager.popBackStackImmediate(entry.getId(), flags);
- return;
- }
- }
- }
- }
-
- /**
- * Returns true if allows focus out of start edge of GuidedStepFragment, false otherwise.
- * Default value is false, the reason is to disable FocusFinder to find focusable views
- * beneath content of GuidedStepFragment. Subclass may override.
- * @return True if allows focus out of start edge of GuidedStepFragment.
- */
- public boolean isFocusOutStartAllowed() {
- return false;
- }
-
- /**
- * Returns true if allows focus out of end edge of GuidedStepFragment, false otherwise.
- * Default value is false, the reason is to disable FocusFinder to find focusable views
- * beneath content of GuidedStepFragment. Subclass may override.
- * @return True if allows focus out of end edge of GuidedStepFragment.
- */
- public boolean isFocusOutEndAllowed() {
- return false;
- }
-
- /**
- * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation.
- * Currently we provide 2 different variations for animation - slide in from
- * side (default) or bottom.
- *
- * Ideally we can retrieve the screen mode settings from the theme attribute
- * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to
- * determine the transition. But the fragment context to retrieve the theme
- * isn't available on platform v23 or earlier.
- *
- * For now clients(subclasses) can call this method inside the constructor.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setEntranceTransitionType(int transitionType) {
- this.entranceTransitionType = transitionType;
- }
-
- /**
- * Opens the provided action in edit mode and raises ime. This can be
- * used to programmatically skip the extra click required to go into edit mode. This method
- * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
- */
- public void openInEditMode(GuidedAction action) {
- mActionsStylist.openInEditMode(action);
- }
-
- private void resolveTheme() {
- // Look up the guidedStepTheme in the currently specified theme. If it exists,
- // replace the theme with its value.
- Context context = FragmentUtil.getContext(GuidedStepFragment.this);
- int theme = onProvideTheme();
- if (theme == -1 && !isGuidedStepTheme(context)) {
- // Look up the guidedStepTheme in the activity's currently specified theme. If it
- // exists, replace the theme with its value.
- int resId = R.attr.guidedStepTheme;
- TypedValue typedValue = new TypedValue();
- boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
- if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
- if (found) {
- ContextThemeWrapper themeWrapper =
- new ContextThemeWrapper(context, typedValue.resourceId);
- if (isGuidedStepTheme(themeWrapper)) {
- mThemeWrapper = themeWrapper;
- } else {
- found = false;
- mThemeWrapper = null;
- }
- }
- if (!found) {
- Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
- }
- } else if (theme != -1) {
- mThemeWrapper = new ContextThemeWrapper(context, theme);
- }
- }
-
- private LayoutInflater getThemeInflater(LayoutInflater inflater) {
- if (mThemeWrapper == null) {
- return inflater;
- } else {
- return inflater.cloneInContext(mThemeWrapper);
- }
- }
-
- private int getFirstCheckedAction() {
- for (int i = 0, size = mActions.size(); i < size; i++) {
- if (mActions.get(i).isChecked()) {
- return i;
- }
- }
- return 0;
- }
-
- void runImeAnimations(boolean entering) {
- ArrayList<Animator> animators = new ArrayList<Animator>();
- if (entering) {
- mGuidanceStylist.onImeAppearing(animators);
- mActionsStylist.onImeAppearing(animators);
- mButtonActionsStylist.onImeAppearing(animators);
- } else {
- mGuidanceStylist.onImeDisappearing(animators);
- mActionsStylist.onImeDisappearing(animators);
- mButtonActionsStylist.onImeDisappearing(animators);
- }
- AnimatorSet set = new AnimatorSet();
- set.playTogether(animators);
- set.start();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
deleted file mode 100644
index aeb2d33..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
+++ /dev/null
@@ -1,1400 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionAdapter;
-import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentManager.BackStackEntry;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A GuidedStepSupportFragment is used to guide the user through a decision or series of decisions.
- * It is composed of a guidance view on the left and a view on the right containing a list of
- * possible actions.
- * <p>
- * <h3>Basic Usage</h3>
- * <p>
- * Clients of GuidedStepSupportFragment must create a custom subclass to attach to their Activities.
- * This custom subclass provides the information necessary to construct the user interface and
- * respond to user actions. At a minimum, subclasses should override:
- * <ul>
- * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
- * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
- * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
- * </ul>
- * <p>
- * Clients use following helper functions to add GuidedStepSupportFragment to Activity or FragmentManager:
- * <ul>
- * <li>{@link #addAsRoot(FragmentActivity, GuidedStepSupportFragment, int)}, to be called during Activity onCreate,
- * adds GuidedStepSupportFragment as the first Fragment in activity.</li>
- * <li>{@link #add(FragmentManager, GuidedStepSupportFragment)} or {@link #add(FragmentManager,
- * GuidedStepSupportFragment, int)}, to add GuidedStepSupportFragment on top of existing Fragments or
- * replacing existing GuidedStepSupportFragment when moving forward to next step.</li>
- * <li>{@link #finishGuidedStepSupportFragments()} can either finish the activity or pop all
- * GuidedStepSupportFragment from stack.
- * <li>If app chooses not to use the helper function, it is the app's responsibility to call
- * {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
- * need pops to.
- * </ul>
- * <h3>Theming and Stylists</h3>
- * <p>
- * GuidedStepSupportFragment delegates its visual styling to classes called stylists. The {@link
- * GuidanceStylist} is responsible for the left guidance view, while the {@link
- * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
- * attributes to derive values associated with the presentation, such as colors, animations, etc.
- * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
- * via theming; see their documentation for more information.
- * <p>
- * GuidedStepSupportFragments must have access to an appropriate theme in order for the stylists to
- * function properly. Specifically, the fragment must receive {@link
- * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
- * is set to that theme. Themes can be provided in one of three ways:
- * <ul>
- * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
- * theme that derives from it.</li>
- * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
- * existing Activity theme can have an entry added for the attribute {@link
- * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
- * this theme will be used by GuidedStepSupportFragment as an overlay to the Activity's theme.</li>
- * <li>Finally, custom subclasses of GuidedStepSupportFragment may provide a theme through the {@link
- * #onProvideTheme} method. This can be useful if a subclass is used across multiple
- * Activities.</li>
- * </ul>
- * <p>
- * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
- * the Activity's theme. (Themes whose parent theme is already set to the guided step theme do not
- * need to set the guidedStepTheme attribute; if set, it will be ignored.)
- * <p>
- * If themes do not provide enough customizability, the stylists themselves may be subclassed and
- * provided to the GuidedStepSupportFragment through the {@link #onCreateGuidanceStylist} and {@link
- * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses
- * may override layout files; subclasses may also have more complex logic to determine styling.
- * <p>
- * <h3>Guided sequences</h3>
- * <p>
- * GuidedStepSupportFragments can be grouped together to provide a guided sequence. GuidedStepSupportFragments
- * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
- * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
- * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
- * custom animations are properly configured. (Custom animations are triggered automatically when
- * the fragment stack is subsequently popped by any normal mechanism.)
- * <p>
- * <i>Note: Currently GuidedStepSupportFragments grouped in this way must all be defined programmatically,
- * rather than in XML. This restriction may be removed in the future.</i>
- *
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepBackground
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeight
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthWeightTwoPanels
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackground
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsBackgroundDark
- * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsElevation
- * @see GuidanceStylist
- * @see GuidanceStylist.Guidance
- * @see GuidedAction
- * @see GuidedActionsStylist
- */
-public class GuidedStepSupportFragment extends Fragment implements GuidedActionAdapter.FocusListener {
-
- private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepSupportFragment";
- private static final String EXTRA_ACTION_PREFIX = "action_";
- private static final String EXTRA_BUTTON_ACTION_PREFIX = "buttonaction_";
-
- private static final String ENTRY_NAME_REPLACE = "GuidedStepDefault";
-
- private static final String ENTRY_NAME_ENTRANCE = "GuidedStepEntrance";
-
- private static final boolean IS_FRAMEWORK_FRAGMENT = false;
-
- /**
- * Fragment argument name for UI style. The argument value is persisted in fragment state and
- * used to select fragment transition. The value is initially {@link #UI_STYLE_ENTRANCE} and
- * might be changed in one of the three helper functions:
- * <ul>
- * <li>{@link #addAsRoot(FragmentActivity, GuidedStepSupportFragment, int)} sets to
- * {@link #UI_STYLE_ACTIVITY_ROOT}</li>
- * <li>{@link #add(FragmentManager, GuidedStepSupportFragment)} or {@link #add(FragmentManager,
- * GuidedStepSupportFragment, int)} sets it to {@link #UI_STYLE_REPLACE} if there is already a
- * GuidedStepSupportFragment on stack.</li>
- * <li>{@link #finishGuidedStepSupportFragments()} changes current GuidedStepSupportFragment to
- * {@link #UI_STYLE_ENTRANCE} for the non activity case. This is a special case that changes
- * the transition settings after fragment has been created, in order to force current
- * GuidedStepSupportFragment run a return transition of {@link #UI_STYLE_ENTRANCE}</li>
- * </ul>
- * <p>
- * Argument value can be either:
- * <ul>
- * <li>{@link #UI_STYLE_REPLACE}</li>
- * <li>{@link #UI_STYLE_ENTRANCE}</li>
- * <li>{@link #UI_STYLE_ACTIVITY_ROOT}</li>
- * </ul>
- */
- public static final String EXTRA_UI_STYLE = "uiStyle";
-
- /**
- * This is the case that we use GuidedStepSupportFragment to replace another existing
- * GuidedStepSupportFragment when moving forward to next step. Default behavior of this style is:
- * <ul>
- * <li>Enter transition slides in from END(right), exit transition same as
- * {@link #UI_STYLE_ENTRANCE}.
- * </li>
- * </ul>
- */
- public static final int UI_STYLE_REPLACE = 0;
-
- /**
- * @deprecated Same value as {@link #UI_STYLE_REPLACE}.
- */
- @Deprecated
- public static final int UI_STYLE_DEFAULT = 0;
-
- /**
- * Default value for argument {@link #EXTRA_UI_STYLE}. The default value is assigned in
- * GuidedStepSupportFragment constructor. This is the case that we show GuidedStepSupportFragment on top of
- * other content. The default behavior of this style:
- * <ul>
- * <li>Enter transition slides in from two sides, exit transition slide out to START(left).
- * Background will be faded in. Note: Changing exit transition by UI style is not working
- * because fragment transition asks for exit transition before UI style is restored in Fragment
- * .onCreate().</li>
- * </ul>
- * When popping multiple GuidedStepSupportFragment, {@link #finishGuidedStepSupportFragments()} also changes
- * the top GuidedStepSupportFragment to UI_STYLE_ENTRANCE in order to run the return transition
- * (reverse of enter transition) of UI_STYLE_ENTRANCE.
- */
- public static final int UI_STYLE_ENTRANCE = 1;
-
- /**
- * One possible value of argument {@link #EXTRA_UI_STYLE}. This is the case that we show first
- * GuidedStepSupportFragment in a separate activity. The default behavior of this style:
- * <ul>
- * <li>Enter transition is assigned null (will rely on activity transition), exit transition is
- * same as {@link #UI_STYLE_ENTRANCE}. Note: Changing exit transition by UI style is not working
- * because fragment transition asks for exit transition before UI style is restored in
- * Fragment.onCreate().</li>
- * </ul>
- */
- public static final int UI_STYLE_ACTIVITY_ROOT = 2;
-
- /**
- * Animation to slide the contents from the side (left/right).
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final int SLIDE_FROM_SIDE = 0;
-
- /**
- * Animation to slide the contents from the bottom.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final int SLIDE_FROM_BOTTOM = 1;
-
- private static final String TAG = "GuidedStepF";
- private static final boolean DEBUG = false;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static class DummyFragment extends Fragment {
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- final View v = new View(inflater.getContext());
- v.setVisibility(View.GONE);
- return v;
- }
- }
-
- private ContextThemeWrapper mThemeWrapper;
- private GuidanceStylist mGuidanceStylist;
- GuidedActionsStylist mActionsStylist;
- private GuidedActionsStylist mButtonActionsStylist;
- private GuidedActionAdapter mAdapter;
- private GuidedActionAdapter mSubAdapter;
- private GuidedActionAdapter mButtonAdapter;
- private GuidedActionAdapterGroup mAdapterGroup;
- private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
- private List<GuidedAction> mButtonActions = new ArrayList<GuidedAction>();
- private int entranceTransitionType = SLIDE_FROM_SIDE;
-
- public GuidedStepSupportFragment() {
- mGuidanceStylist = onCreateGuidanceStylist();
- mActionsStylist = onCreateActionsStylist();
- mButtonActionsStylist = onCreateButtonActionsStylist();
- onProvideFragmentTransitions();
- }
-
- /**
- * Creates the presenter used to style the guidance panel. The default implementation returns
- * a basic GuidanceStylist.
- * @return The GuidanceStylist used in this fragment.
- */
- public GuidanceStylist onCreateGuidanceStylist() {
- return new GuidanceStylist();
- }
-
- /**
- * Creates the presenter used to style the guided actions panel. The default implementation
- * returns a basic GuidedActionsStylist.
- * @return The GuidedActionsStylist used in this fragment.
- */
- public GuidedActionsStylist onCreateActionsStylist() {
- return new GuidedActionsStylist();
- }
-
- /**
- * Creates the presenter used to style a sided actions panel for button only.
- * The default implementation returns a basic GuidedActionsStylist.
- * @return The GuidedActionsStylist used in this fragment.
- */
- public GuidedActionsStylist onCreateButtonActionsStylist() {
- GuidedActionsStylist stylist = new GuidedActionsStylist();
- stylist.setAsButtonActions();
- return stylist;
- }
-
- /**
- * Returns the theme used for styling the fragment. The default returns -1, indicating that the
- * host Activity's theme should be used.
- * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
- * host Activity's theme.
- */
- public int onProvideTheme() {
- return -1;
- }
-
- /**
- * Returns the information required to provide guidance to the user. This hook is called during
- * {@link #onCreateView}. May be overridden to return a custom subclass of {@link
- * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
- * returns a Guidance object with empty fields; subclasses should override.
- * @param savedInstanceState The saved instance state from onCreateView.
- * @return The Guidance object representing the information used to guide the user.
- */
- public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
- return new Guidance("", "", "", null);
- }
-
- /**
- * Fills out the set of actions available to the user. This hook is called during {@link
- * #onCreate}. The default leaves the list of actions empty; subclasses should override.
- * @param actions A non-null, empty list ready to be populated.
- * @param savedInstanceState The saved instance state from onCreate.
- */
- public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- /**
- * Fills out the set of actions shown at right available to the user. This hook is called during
- * {@link #onCreate}. The default leaves the list of actions empty; subclasses may override.
- * @param actions A non-null, empty list ready to be populated.
- * @param savedInstanceState The saved instance state from onCreate.
- */
- public void onCreateButtonActions(@NonNull List<GuidedAction> actions,
- Bundle savedInstanceState) {
- }
-
- /**
- * Callback invoked when an action is taken by the user. Subclasses should override in
- * order to act on the user's decisions.
- * @param action The chosen action.
- */
- public void onGuidedActionClicked(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action in sub actions is taken by the user. Subclasses should
- * override in order to act on the user's decisions. Default return value is true to close
- * the sub actions list.
- * @param action The chosen action.
- * @return true to collapse the sub actions list, false to keep it expanded.
- */
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return true;
- }
-
- /**
- * @return True if is current expanded including subactions list or
- * action with {@link GuidedAction#hasEditableActivatorView()} is true.
- */
- public boolean isExpanded() {
- return mActionsStylist.isExpanded();
- }
-
- /**
- * @return True if the sub actions list is expanded, false otherwise.
- */
- public boolean isSubActionsExpanded() {
- return mActionsStylist.isSubActionsExpanded();
- }
-
- /**
- * Expand a given action's sub actions list.
- * @param action GuidedAction to expand.
- * @see #expandAction(GuidedAction, boolean)
- */
- public void expandSubActions(GuidedAction action) {
- if (!action.hasSubActions()) {
- return;
- }
- expandAction(action, true);
- }
-
- /**
- * Expand a given action with sub actions list or
- * {@link GuidedAction#hasEditableActivatorView()} is true. The method must be called after
- * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} creates fragment view.
- *
- * @param action GuidedAction to expand.
- * @param withTransition True to run transition animation, false otherwise.
- */
- public void expandAction(GuidedAction action, boolean withTransition) {
- mActionsStylist.expandAction(action, withTransition);
- }
-
- /**
- * Collapse sub actions list.
- * @see GuidedAction#getSubActions()
- */
- public void collapseSubActions() {
- collapseAction(true);
- }
-
- /**
- * Collapse action which either has a sub actions list or action with
- * {@link GuidedAction#hasEditableActivatorView()} is true.
- *
- * @param withTransition True to run transition animation, false otherwise.
- */
- public void collapseAction(boolean withTransition) {
- if (mActionsStylist != null && mActionsStylist.getActionsGridView() != null) {
- mActionsStylist.collapseAction(withTransition);
- }
- }
-
- /**
- * Callback invoked when an action is focused (made to be the current selection) by the user.
- */
- @Override
- public void onGuidedActionFocused(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action's title or description has been edited, this happens either
- * when user clicks confirm button in IME or user closes IME window by BACK key.
- * @deprecated Override {@link #onGuidedActionEditedAndProceed(GuidedAction)} and/or
- * {@link #onGuidedActionEditCanceled(GuidedAction)}.
- */
- @Deprecated
- public void onGuidedActionEdited(GuidedAction action) {
- }
-
- /**
- * Callback invoked when an action has been canceled editing, for example when user closes
- * IME window by BACK key. Default implementation calls deprecated method
- * {@link #onGuidedActionEdited(GuidedAction)}.
- * @param action The action which has been canceled editing.
- */
- public void onGuidedActionEditCanceled(GuidedAction action) {
- onGuidedActionEdited(action);
- }
-
- /**
- * Callback invoked when an action has been edited, for example when user clicks confirm button
- * in IME window. Default implementation calls deprecated method
- * {@link #onGuidedActionEdited(GuidedAction)} and returns {@link GuidedAction#ACTION_ID_NEXT}.
- *
- * @param action The action that has been edited.
- * @return ID of the action will be focused or {@link GuidedAction#ACTION_ID_NEXT},
- * {@link GuidedAction#ACTION_ID_CURRENT}.
- */
- public long onGuidedActionEditedAndProceed(GuidedAction action) {
- onGuidedActionEdited(action);
- return GuidedAction.ACTION_ID_NEXT;
- }
-
- /**
- * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
- * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
- * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
- * is pressed.
- * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE}
- * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
- * <p>
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param fragmentManager The FragmentManager to be used in the transaction.
- * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
- * @return The ID returned by the call FragmentTransaction.commit.
- */
- public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment) {
- return add(fragmentManager, fragment, android.R.id.content);
- }
-
- /**
- * Adds the specified GuidedStepSupportFragment to the fragment stack, replacing any existing
- * GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
- * transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
- * is pressed.
- * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE} and
- * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepSupportFragment)} will be called
- * to perform shared element transition between GuidedStepSupportFragments.
- * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
- * <p>
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param fragmentManager The FragmentManager to be used in the transaction.
- * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
- * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
- * @return The ID returned by the call FragmentTransaction.commit.
- */
- public static int add(FragmentManager fragmentManager, GuidedStepSupportFragment fragment, int id) {
- GuidedStepSupportFragment current = getCurrentGuidedStepSupportFragment(fragmentManager);
- boolean inGuidedStep = current != null;
- if (IS_FRAMEWORK_FRAGMENT && Build.VERSION.SDK_INT >= 21 && Build.VERSION.SDK_INT < 23
- && !inGuidedStep) {
- // workaround b/22631964 for framework fragment
- fragmentManager.beginTransaction()
- .replace(id, new DummyFragment(), TAG_LEAN_BACK_ACTIONS_FRAGMENT)
- .commit();
- }
- FragmentTransaction ft = fragmentManager.beginTransaction();
-
- fragment.setUiStyle(inGuidedStep ? UI_STYLE_REPLACE : UI_STYLE_ENTRANCE);
- ft.addToBackStack(fragment.generateStackEntryName());
- if (current != null) {
- fragment.onAddSharedElementTransition(ft, current);
- }
- return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
- }
-
- /**
- * Called when this fragment is added to FragmentTransaction with {@link #UI_STYLE_REPLACE} (aka
- * when the GuidedStepSupportFragment replacing an existing GuidedStepSupportFragment). Default implementation
- * establishes connections between action background views to morph action background bounds
- * change from disappearing GuidedStepSupportFragment into this GuidedStepSupportFragment. The default
- * implementation heavily relies on {@link GuidedActionsStylist}'s layout, app may override this
- * method when modifying the default layout of {@link GuidedActionsStylist}.
- *
- * @see GuidedActionsStylist
- * @see #onProvideFragmentTransitions()
- * @param ft The FragmentTransaction to add shared element.
- * @param disappearing The disappearing fragment.
- */
- protected void onAddSharedElementTransition(FragmentTransaction ft, GuidedStepSupportFragment
- disappearing) {
- View fragmentView = disappearing.getView();
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment_root), "action_fragment_root");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment_background), "action_fragment_background");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.action_fragment), "action_fragment");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_root), "guidedactions_root");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_content), "guidedactions_content");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_list_background), "guidedactions_list_background");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_root2), "guidedactions_root2");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_content2), "guidedactions_content2");
- addNonNullSharedElementTransition(ft, fragmentView.findViewById(
- R.id.guidedactions_list_background2), "guidedactions_list_background2");
- }
-
- private static void addNonNullSharedElementTransition (FragmentTransaction ft, View subView,
- String transitionName)
- {
- if (subView != null)
- TransitionHelper.addSharedElement(ft, subView, transitionName);
- }
-
- /**
- * Returns BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
- * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} will return empty String. The method
- * returns undefined value if the fragment is not in FragmentManager.
- * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
- * associated.
- */
- final String generateStackEntryName() {
- return generateStackEntryName(getUiStyle(), getClass());
- }
-
- /**
- * Generates BackStackEntry name for GuidedStepSupportFragment class or empty String if no entry is
- * associated. Note {@link #UI_STYLE_ACTIVITY_ROOT} is not allowed and returns empty String.
- * @param uiStyle {@link #UI_STYLE_REPLACE} or {@link #UI_STYLE_ENTRANCE}
- * @return BackStackEntry name for the GuidedStepSupportFragment or empty String if no entry is
- * associated.
- */
- static String generateStackEntryName(int uiStyle, Class guidedStepFragmentClass) {
- switch (uiStyle) {
- case UI_STYLE_REPLACE:
- return ENTRY_NAME_REPLACE + guidedStepFragmentClass.getName();
- case UI_STYLE_ENTRANCE:
- return ENTRY_NAME_ENTRANCE + guidedStepFragmentClass.getName();
- case UI_STYLE_ACTIVITY_ROOT:
- default:
- return "";
- }
- }
-
- /**
- * Returns true if the backstack entry represents GuidedStepSupportFragment with
- * {@link #UI_STYLE_ENTRANCE}, i.e. this is the first GuidedStepSupportFragment pushed to stack; false
- * otherwise.
- * @see #generateStackEntryName(int, Class)
- * @param backStackEntryName Name of BackStackEntry.
- * @return True if the backstack represents GuidedStepSupportFragment with {@link #UI_STYLE_ENTRANCE};
- * false otherwise.
- */
- static boolean isStackEntryUiStyleEntrance(String backStackEntryName) {
- return backStackEntryName != null && backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE);
- }
-
- /**
- * Extract Class name from BackStackEntry name.
- * @param backStackEntryName Name of BackStackEntry.
- * @return Class name of GuidedStepSupportFragment.
- */
- static String getGuidedStepSupportFragmentClassName(String backStackEntryName) {
- if (backStackEntryName.startsWith(ENTRY_NAME_REPLACE)) {
- return backStackEntryName.substring(ENTRY_NAME_REPLACE.length());
- } else if (backStackEntryName.startsWith(ENTRY_NAME_ENTRANCE)) {
- return backStackEntryName.substring(ENTRY_NAME_ENTRANCE.length());
- } else {
- return "";
- }
- }
-
- /**
- * Adds the specified GuidedStepSupportFragment as content of Activity; no backstack entry is added so
- * the activity will be dismissed when BACK key is pressed. The method is typically called in
- * Activity.onCreate() when savedInstanceState is null. When savedInstanceState is not null,
- * the Activity is being restored, do not call addAsRoot() to duplicate the Fragment restored
- * by FragmentManager.
- * {@link #UI_STYLE_ACTIVITY_ROOT} is assigned.
- *
- * Note: currently fragments added using this method must be created programmatically rather
- * than via XML.
- * @param activity The Activity to be used to insert GuidedstepFragment.
- * @param fragment The GuidedStepSupportFragment to be inserted into the fragment stack.
- * @param id The id of container to add GuidedStepSupportFragment, can be android.R.id.content.
- * @return The ID returned by the call FragmentTransaction.commit, or -1 there is already
- * GuidedStepSupportFragment.
- */
- public static int addAsRoot(FragmentActivity activity, GuidedStepSupportFragment fragment, int id) {
- // Workaround b/23764120: call getDecorView() to force requestFeature of ActivityTransition.
- activity.getWindow().getDecorView();
- FragmentManager fragmentManager = activity.getSupportFragmentManager();
- if (fragmentManager.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT) != null) {
- Log.w(TAG, "Fragment is already exists, likely calling "
- + "addAsRoot() when savedInstanceState is not null in Activity.onCreate().");
- return -1;
- }
- FragmentTransaction ft = fragmentManager.beginTransaction();
- fragment.setUiStyle(UI_STYLE_ACTIVITY_ROOT);
- return ft.replace(id, fragment, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
- }
-
- /**
- * Returns the current GuidedStepSupportFragment on the fragment transaction stack.
- * @return The current GuidedStepSupportFragment, if any, on the fragment transaction stack.
- */
- public static GuidedStepSupportFragment getCurrentGuidedStepSupportFragment(FragmentManager fm) {
- Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
- if (f instanceof GuidedStepSupportFragment) {
- return (GuidedStepSupportFragment) f;
- }
- return null;
- }
-
- /**
- * Returns the GuidanceStylist that displays guidance information for the user.
- * @return The GuidanceStylist for this fragment.
- */
- public GuidanceStylist getGuidanceStylist() {
- return mGuidanceStylist;
- }
-
- /**
- * Returns the GuidedActionsStylist that displays the actions the user may take.
- * @return The GuidedActionsStylist for this fragment.
- */
- public GuidedActionsStylist getGuidedActionsStylist() {
- return mActionsStylist;
- }
-
- /**
- * Returns the list of button GuidedActions that the user may take in this fragment.
- * @return The list of button GuidedActions for this fragment.
- */
- public List<GuidedAction> getButtonActions() {
- return mButtonActions;
- }
-
- /**
- * Find button GuidedAction by Id.
- * @param id Id of the button action to search.
- * @return GuidedAction object or null if not found.
- */
- public GuidedAction findButtonActionById(long id) {
- int index = findButtonActionPositionById(id);
- return index >= 0 ? mButtonActions.get(index) : null;
- }
-
- /**
- * Find button GuidedAction position in array by Id.
- * @param id Id of the button action to search.
- * @return position of GuidedAction object in array or -1 if not found.
- */
- public int findButtonActionPositionById(long id) {
- if (mButtonActions != null) {
- for (int i = 0; i < mButtonActions.size(); i++) {
- GuidedAction action = mButtonActions.get(i);
- if (mButtonActions.get(i).getId() == id) {
- return i;
- }
- }
- }
- return -1;
- }
-
- /**
- * Returns the GuidedActionsStylist that displays the button actions the user may take.
- * @return The GuidedActionsStylist for this fragment.
- */
- public GuidedActionsStylist getGuidedButtonActionsStylist() {
- return mButtonActionsStylist;
- }
-
- /**
- * Sets the list of button GuidedActions that the user may take in this fragment.
- * @param actions The list of button GuidedActions for this fragment.
- */
- public void setButtonActions(List<GuidedAction> actions) {
- mButtonActions = actions;
- if (mButtonAdapter != null) {
- mButtonAdapter.setActions(mButtonActions);
- }
- }
-
- /**
- * Notify an button action has changed and update its UI.
- * @param position Position of the button GuidedAction in array.
- */
- public void notifyButtonActionChanged(int position) {
- if (mButtonAdapter != null) {
- mButtonAdapter.notifyItemChanged(position);
- }
- }
-
- /**
- * Returns the view corresponding to the button action at the indicated position in the list of
- * actions for this fragment.
- * @param position The integer position of the button action of interest.
- * @return The View corresponding to the button action at the indicated position, or null if
- * that action is not currently onscreen.
- */
- public View getButtonActionItemView(int position) {
- final RecyclerView.ViewHolder holder = mButtonActionsStylist.getActionsGridView()
- .findViewHolderForPosition(position);
- return holder == null ? null : holder.itemView;
- }
-
- /**
- * Scrolls the action list to the position indicated, selecting that button action's view.
- * @param position The integer position of the button action of interest.
- */
- public void setSelectedButtonActionPosition(int position) {
- mButtonActionsStylist.getActionsGridView().setSelectedPosition(position);
- }
-
- /**
- * Returns the position if the currently selected button GuidedAction.
- * @return position The integer position of the currently selected button action.
- */
- public int getSelectedButtonActionPosition() {
- return mButtonActionsStylist.getActionsGridView().getSelectedPosition();
- }
-
- /**
- * Returns the list of GuidedActions that the user may take in this fragment.
- * @return The list of GuidedActions for this fragment.
- */
- public List<GuidedAction> getActions() {
- return mActions;
- }
-
- /**
- * Find GuidedAction by Id.
- * @param id Id of the action to search.
- * @return GuidedAction object or null if not found.
- */
- public GuidedAction findActionById(long id) {
- int index = findActionPositionById(id);
- return index >= 0 ? mActions.get(index) : null;
- }
-
- /**
- * Find GuidedAction position in array by Id.
- * @param id Id of the action to search.
- * @return position of GuidedAction object in array or -1 if not found.
- */
- public int findActionPositionById(long id) {
- if (mActions != null) {
- for (int i = 0; i < mActions.size(); i++) {
- GuidedAction action = mActions.get(i);
- if (mActions.get(i).getId() == id) {
- return i;
- }
- }
- }
- return -1;
- }
-
- /**
- * Sets the list of GuidedActions that the user may take in this fragment.
- * @param actions The list of GuidedActions for this fragment.
- */
- public void setActions(List<GuidedAction> actions) {
- mActions = actions;
- if (mAdapter != null) {
- mAdapter.setActions(mActions);
- }
- }
-
- /**
- * Notify an action has changed and update its UI.
- * @param position Position of the GuidedAction in array.
- */
- public void notifyActionChanged(int position) {
- if (mAdapter != null) {
- mAdapter.notifyItemChanged(position);
- }
- }
-
- /**
- * Returns the view corresponding to the action at the indicated position in the list of
- * actions for this fragment.
- * @param position The integer position of the action of interest.
- * @return The View corresponding to the action at the indicated position, or null if that
- * action is not currently onscreen.
- */
- public View getActionItemView(int position) {
- final RecyclerView.ViewHolder holder = mActionsStylist.getActionsGridView()
- .findViewHolderForPosition(position);
- return holder == null ? null : holder.itemView;
- }
-
- /**
- * Scrolls the action list to the position indicated, selecting that action's view.
- * @param position The integer position of the action of interest.
- */
- public void setSelectedActionPosition(int position) {
- mActionsStylist.getActionsGridView().setSelectedPosition(position);
- }
-
- /**
- * Returns the position if the currently selected GuidedAction.
- * @return position The integer position of the currently selected action.
- */
- public int getSelectedActionPosition() {
- return mActionsStylist.getActionsGridView().getSelectedPosition();
- }
-
- /**
- * Called by Constructor to provide fragment transitions. The default implementation assigns
- * transitions based on {@link #getUiStyle()}:
- * <ul>
- * <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
- * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
- * <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
- * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
- * <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
- * activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
- * enter transition.
- * </ul>
- * <p>
- * The default implementation heavily relies on {@link GuidedActionsStylist} and
- * {@link GuidanceStylist} layout, app may override this method when modifying the default
- * layout of {@link GuidedActionsStylist} or {@link GuidanceStylist}.
- * <p>
- * TIP: because the fragment view is removed during fragment transition, in general app cannot
- * use two Visibility transition together. Workaround is to create your own Visibility
- * transition that controls multiple animators (e.g. slide and fade animation in one Transition
- * class).
- */
- protected void onProvideFragmentTransitions() {
- if (Build.VERSION.SDK_INT >= 21) {
- final int uiStyle = getUiStyle();
- if (uiStyle == UI_STYLE_REPLACE) {
- Object enterTransition = TransitionHelper.createFadeAndShortSlide(Gravity.END);
- TransitionHelper.exclude(enterTransition, R.id.guidedstep_background, true);
- TransitionHelper.exclude(enterTransition, R.id.guidedactions_sub_list_background,
- true);
- TransitionHelper.setEnterTransition(this, enterTransition);
-
- Object fade = TransitionHelper.createFadeTransition(
- TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
- TransitionHelper.include(fade, R.id.guidedactions_sub_list_background);
- Object changeBounds = TransitionHelper.createChangeBounds(false);
- Object sharedElementTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(sharedElementTransition, fade);
- TransitionHelper.addTransition(sharedElementTransition, changeBounds);
- TransitionHelper.setSharedElementEnterTransition(this, sharedElementTransition);
- } else if (uiStyle == UI_STYLE_ENTRANCE) {
- if (entranceTransitionType == SLIDE_FROM_SIDE) {
- Object fade = TransitionHelper.createFadeTransition(
- TransitionHelper.FADE_IN | TransitionHelper.FADE_OUT);
- TransitionHelper.include(fade, R.id.guidedstep_background);
- Object slideFromSide = TransitionHelper.createFadeAndShortSlide(
- Gravity.END | Gravity.START);
- TransitionHelper.include(slideFromSide, R.id.content_fragment);
- TransitionHelper.include(slideFromSide, R.id.action_fragment_root);
- Object enterTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(enterTransition, fade);
- TransitionHelper.addTransition(enterTransition, slideFromSide);
- TransitionHelper.setEnterTransition(this, enterTransition);
- } else {
- Object slideFromBottom = TransitionHelper.createFadeAndShortSlide(
- Gravity.BOTTOM);
- TransitionHelper.include(slideFromBottom, R.id.guidedstep_background_view_root);
- Object enterTransition = TransitionHelper.createTransitionSet(false);
- TransitionHelper.addTransition(enterTransition, slideFromBottom);
- TransitionHelper.setEnterTransition(this, enterTransition);
- }
- // No shared element transition
- TransitionHelper.setSharedElementEnterTransition(this, null);
- } else if (uiStyle == UI_STYLE_ACTIVITY_ROOT) {
- // for Activity root, we don't need enter transition, use activity transition
- TransitionHelper.setEnterTransition(this, null);
- // No shared element transition
- TransitionHelper.setSharedElementEnterTransition(this, null);
- }
- // exitTransition is same for all style
- Object exitTransition = TransitionHelper.createFadeAndShortSlide(Gravity.START);
- TransitionHelper.exclude(exitTransition, R.id.guidedstep_background, true);
- TransitionHelper.exclude(exitTransition, R.id.guidedactions_sub_list_background,
- true);
- TransitionHelper.setExitTransition(this, exitTransition);
- }
- }
-
- /**
- * Called by onCreateView to inflate background view. Default implementation loads view
- * from {@link R.layout#lb_guidedstep_background} which holds a reference to
- * guidedStepBackground.
- * @param inflater LayoutInflater to load background view.
- * @param container Parent view of background view.
- * @param savedInstanceState
- * @return Created background view or null if no background.
- */
- public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.lb_guidedstep_background, container, false);
- }
-
- /**
- * Set UI style to fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when fragment
- * is first initialized. UI style is used to choose different fragment transition animations and
- * determine if this is the first GuidedStepSupportFragment on backstack. In most cases app does not
- * directly call this method, app calls helper function
- * {@link #add(FragmentManager, GuidedStepSupportFragment, int)}. However if the app creates Fragment
- * transaction and controls backstack by itself, it would need call setUiStyle() to select the
- * fragment transition to use.
- *
- * @param style {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
- * {@link #UI_STYLE_ENTRANCE}.
- */
- public void setUiStyle(int style) {
- int oldStyle = getUiStyle();
- Bundle arguments = getArguments();
- boolean isNew = false;
- if (arguments == null) {
- arguments = new Bundle();
- isNew = true;
- }
- arguments.putInt(EXTRA_UI_STYLE, style);
- // call setArgument() will validate if the fragment is already added.
- if (isNew) {
- setArguments(arguments);
- }
- if (style != oldStyle) {
- onProvideFragmentTransitions();
- }
- }
-
- /**
- * Read UI style from fragment arguments. Default value is {@link #UI_STYLE_ENTRANCE} when
- * fragment is first initialized. UI style is used to choose different fragment transition
- * animations and determine if this is the first GuidedStepSupportFragment on backstack.
- *
- * @return {@link #UI_STYLE_ACTIVITY_ROOT} {@link #UI_STYLE_REPLACE} or
- * {@link #UI_STYLE_ENTRANCE}.
- * @see #onProvideFragmentTransitions()
- */
- public int getUiStyle() {
- Bundle b = getArguments();
- if (b == null) return UI_STYLE_ENTRANCE;
- return b.getInt(EXTRA_UI_STYLE, UI_STYLE_ENTRANCE);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (DEBUG) Log.v(TAG, "onCreate");
- // Set correct transition from saved arguments.
- onProvideFragmentTransitions();
-
- ArrayList<GuidedAction> actions = new ArrayList<GuidedAction>();
- onCreateActions(actions, savedInstanceState);
- if (savedInstanceState != null) {
- onRestoreActions(actions, savedInstanceState);
- }
- setActions(actions);
- ArrayList<GuidedAction> buttonActions = new ArrayList<GuidedAction>();
- onCreateButtonActions(buttonActions, savedInstanceState);
- if (savedInstanceState != null) {
- onRestoreButtonActions(buttonActions, savedInstanceState);
- }
- setButtonActions(buttonActions);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onDestroyView() {
- mGuidanceStylist.onDestroyView();
- mActionsStylist.onDestroyView();
- mButtonActionsStylist.onDestroyView();
- mAdapter = null;
- mSubAdapter = null;
- mButtonAdapter = null;
- mAdapterGroup = null;
- super.onDestroyView();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- if (DEBUG) Log.v(TAG, "onCreateView");
-
- resolveTheme();
- inflater = getThemeInflater(inflater);
-
- GuidedStepRootLayout root = (GuidedStepRootLayout) inflater.inflate(
- R.layout.lb_guidedstep_fragment, container, false);
-
- root.setFocusOutStart(isFocusOutStartAllowed());
- root.setFocusOutEnd(isFocusOutEndAllowed());
-
- ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
- ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
- ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
-
- Guidance guidance = onCreateGuidance(savedInstanceState);
- View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
- guidanceContainer.addView(guidanceView);
-
- View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
- actionContainer.addView(actionsView);
-
- View buttonActionsView = mButtonActionsStylist.onCreateView(inflater, actionContainer);
- actionContainer.addView(buttonActionsView);
-
- GuidedActionAdapter.EditListener editListener = new GuidedActionAdapter.EditListener() {
-
- @Override
- public void onImeOpen() {
- runImeAnimations(true);
- }
-
- @Override
- public void onImeClose() {
- runImeAnimations(false);
- }
-
- @Override
- public long onGuidedActionEditedAndProceed(GuidedAction action) {
- return GuidedStepSupportFragment.this.onGuidedActionEditedAndProceed(action);
- }
-
- @Override
- public void onGuidedActionEditCanceled(GuidedAction action) {
- GuidedStepSupportFragment.this.onGuidedActionEditCanceled(action);
- }
- };
-
- mAdapter = new GuidedActionAdapter(mActions, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- GuidedStepSupportFragment.this.onGuidedActionClicked(action);
- if (isExpanded()) {
- collapseAction(true);
- } else if (action.hasSubActions() || action.hasEditableActivatorView()) {
- expandAction(action, true);
- }
- }
- }, this, mActionsStylist, false);
- mButtonAdapter =
- new GuidedActionAdapter(mButtonActions, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- GuidedStepSupportFragment.this.onGuidedActionClicked(action);
- }
- }, this, mButtonActionsStylist, false);
- mSubAdapter = new GuidedActionAdapter(null, new GuidedActionAdapter.ClickListener() {
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (mActionsStylist.isInExpandTransition()) {
- return;
- }
- if (GuidedStepSupportFragment.this.onSubGuidedActionClicked(action)) {
- collapseSubActions();
- }
- }
- }, this, mActionsStylist, true);
- mAdapterGroup = new GuidedActionAdapterGroup();
- mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
- mAdapterGroup.addAdpter(mSubAdapter, null);
- mAdapterGroup.setEditListener(editListener);
- mActionsStylist.setEditListener(editListener);
-
- mActionsStylist.getActionsGridView().setAdapter(mAdapter);
- if (mActionsStylist.getSubActionsGridView() != null) {
- mActionsStylist.getSubActionsGridView().setAdapter(mSubAdapter);
- }
- mButtonActionsStylist.getActionsGridView().setAdapter(mButtonAdapter);
- if (mButtonActions.size() == 0) {
- // when there is no button actions, we don't need show the second panel, but keep
- // the width zero to run ChangeBounds transition.
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
- buttonActionsView.getLayoutParams();
- lp.weight = 0;
- buttonActionsView.setLayoutParams(lp);
- } else {
- // when there are two actions panel, we need adjust the weight of action to
- // guidedActionContentWidthWeightTwoPanels.
- Context ctx = mThemeWrapper != null ? mThemeWrapper : getContext();
- TypedValue typedValue = new TypedValue();
- if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels,
- typedValue, true)) {
- View actionsRoot = root.findViewById(R.id.action_fragment_root);
- float weight = typedValue.getFloat();
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) actionsRoot
- .getLayoutParams();
- lp.weight = weight;
- actionsRoot.setLayoutParams(lp);
- }
- }
-
- // Add the background view.
- View backgroundView = onCreateBackgroundView(inflater, root, savedInstanceState);
- if (backgroundView != null) {
- FrameLayout backgroundViewRoot = (FrameLayout)root.findViewById(
- R.id.guidedstep_background_view_root);
- backgroundViewRoot.addView(backgroundView, 0);
- }
-
- return root;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- getView().findViewById(R.id.action_fragment).requestFocus();
- }
-
- /**
- * Get the key will be used to save GuidedAction with Fragment.
- * @param action GuidedAction to get key.
- * @return Key to save the GuidedAction.
- */
- final String getAutoRestoreKey(GuidedAction action) {
- return EXTRA_ACTION_PREFIX + action.getId();
- }
-
- /**
- * Get the key will be used to save GuidedAction with Fragment.
- * @param action GuidedAction to get key.
- * @return Key to save the GuidedAction.
- */
- final String getButtonAutoRestoreKey(GuidedAction action) {
- return EXTRA_BUTTON_ACTION_PREFIX + action.getId();
- }
-
- final static boolean isSaveEnabled(GuidedAction action) {
- return action.isAutoSaveRestoreEnabled() && action.getId() != GuidedAction.NO_ID;
- }
-
- final void onRestoreActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onRestoreInstanceState(savedInstanceState, getAutoRestoreKey(action));
- }
- }
- }
-
- final void onRestoreButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onRestoreInstanceState(savedInstanceState, getButtonAutoRestoreKey(action));
- }
- }
- }
-
- final void onSaveActions(List<GuidedAction> actions, Bundle outState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onSaveInstanceState(outState, getAutoRestoreKey(action));
- }
- }
- }
-
- final void onSaveButtonActions(List<GuidedAction> actions, Bundle outState) {
- for (int i = 0, size = actions.size(); i < size; i++) {
- GuidedAction action = actions.get(i);
- if (isSaveEnabled(action)) {
- action.onSaveInstanceState(outState, getButtonAutoRestoreKey(action));
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- onSaveActions(mActions, outState);
- onSaveButtonActions(mButtonActions, outState);
- }
-
- private static boolean isGuidedStepTheme(Context context) {
- int resId = R.attr.guidedStepThemeFlag;
- TypedValue typedValue = new TypedValue();
- boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
- if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
- return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
- }
-
- /**
- * Convenient method to close GuidedStepSupportFragments on top of other content or finish Activity if
- * GuidedStepSupportFragments were started in a separate activity. Pops all stack entries including
- * {@link #UI_STYLE_ENTRANCE}; if {@link #UI_STYLE_ENTRANCE} is not found, finish the activity.
- * Note that this method must be paired with {@link #add(FragmentManager, GuidedStepSupportFragment,
- * int)} which sets up the stack entry name for finding which fragment we need to pop back to.
- */
- public void finishGuidedStepSupportFragments() {
- final FragmentManager fragmentManager = getFragmentManager();
- final int entryCount = fragmentManager.getBackStackEntryCount();
- if (entryCount > 0) {
- for (int i = entryCount - 1; i >= 0; i--) {
- BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
- if (isStackEntryUiStyleEntrance(entry.getName())) {
- GuidedStepSupportFragment top = getCurrentGuidedStepSupportFragment(fragmentManager);
- if (top != null) {
- top.setUiStyle(UI_STYLE_ENTRANCE);
- }
- fragmentManager.popBackStackImmediate(entry.getId(),
- FragmentManager.POP_BACK_STACK_INCLUSIVE);
- return;
- }
- }
- }
- ActivityCompat.finishAfterTransition(getActivity());
- }
-
- /**
- * Convenient method to pop to fragment with Given class.
- * @param guidedStepFragmentClass Name of the Class of GuidedStepSupportFragment to pop to.
- * @param flags Either 0 or {@link FragmentManager#POP_BACK_STACK_INCLUSIVE}.
- */
- public void popBackStackToGuidedStepSupportFragment(Class guidedStepFragmentClass, int flags) {
- if (!GuidedStepSupportFragment.class.isAssignableFrom(guidedStepFragmentClass)) {
- return;
- }
- final FragmentManager fragmentManager = getFragmentManager();
- final int entryCount = fragmentManager.getBackStackEntryCount();
- String className = guidedStepFragmentClass.getName();
- if (entryCount > 0) {
- for (int i = entryCount - 1; i >= 0; i--) {
- BackStackEntry entry = fragmentManager.getBackStackEntryAt(i);
- String entryClassName = getGuidedStepSupportFragmentClassName(entry.getName());
- if (className.equals(entryClassName)) {
- fragmentManager.popBackStackImmediate(entry.getId(), flags);
- return;
- }
- }
- }
- }
-
- /**
- * Returns true if allows focus out of start edge of GuidedStepSupportFragment, false otherwise.
- * Default value is false, the reason is to disable FocusFinder to find focusable views
- * beneath content of GuidedStepSupportFragment. Subclass may override.
- * @return True if allows focus out of start edge of GuidedStepSupportFragment.
- */
- public boolean isFocusOutStartAllowed() {
- return false;
- }
-
- /**
- * Returns true if allows focus out of end edge of GuidedStepSupportFragment, false otherwise.
- * Default value is false, the reason is to disable FocusFinder to find focusable views
- * beneath content of GuidedStepSupportFragment. Subclass may override.
- * @return True if allows focus out of end edge of GuidedStepSupportFragment.
- */
- public boolean isFocusOutEndAllowed() {
- return false;
- }
-
- /**
- * Sets the transition type to be used for {@link #UI_STYLE_ENTRANCE} animation.
- * Currently we provide 2 different variations for animation - slide in from
- * side (default) or bottom.
- *
- * Ideally we can retrieve the screen mode settings from the theme attribute
- * {@code Theme.Leanback.GuidedStep#guidedStepHeightWeight} and use that to
- * determine the transition. But the fragment context to retrieve the theme
- * isn't available on platform v23 or earlier.
- *
- * For now clients(subclasses) can call this method inside the constructor.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setEntranceTransitionType(int transitionType) {
- this.entranceTransitionType = transitionType;
- }
-
- /**
- * Opens the provided action in edit mode and raises ime. This can be
- * used to programmatically skip the extra click required to go into edit mode. This method
- * can be invoked in {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
- */
- public void openInEditMode(GuidedAction action) {
- mActionsStylist.openInEditMode(action);
- }
-
- private void resolveTheme() {
- // Look up the guidedStepTheme in the currently specified theme. If it exists,
- // replace the theme with its value.
- Context context = getContext();
- int theme = onProvideTheme();
- if (theme == -1 && !isGuidedStepTheme(context)) {
- // Look up the guidedStepTheme in the activity's currently specified theme. If it
- // exists, replace the theme with its value.
- int resId = R.attr.guidedStepTheme;
- TypedValue typedValue = new TypedValue();
- boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
- if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
- if (found) {
- ContextThemeWrapper themeWrapper =
- new ContextThemeWrapper(context, typedValue.resourceId);
- if (isGuidedStepTheme(themeWrapper)) {
- mThemeWrapper = themeWrapper;
- } else {
- found = false;
- mThemeWrapper = null;
- }
- }
- if (!found) {
- Log.e(TAG, "GuidedStepSupportFragment does not have an appropriate theme set.");
- }
- } else if (theme != -1) {
- mThemeWrapper = new ContextThemeWrapper(context, theme);
- }
- }
-
- private LayoutInflater getThemeInflater(LayoutInflater inflater) {
- if (mThemeWrapper == null) {
- return inflater;
- } else {
- return inflater.cloneInContext(mThemeWrapper);
- }
- }
-
- private int getFirstCheckedAction() {
- for (int i = 0, size = mActions.size(); i < size; i++) {
- if (mActions.get(i).isChecked()) {
- return i;
- }
- }
- return 0;
- }
-
- void runImeAnimations(boolean entering) {
- ArrayList<Animator> animators = new ArrayList<Animator>();
- if (entering) {
- mGuidanceStylist.onImeAppearing(animators);
- mActionsStylist.onImeAppearing(animators);
- mButtonActionsStylist.onImeAppearing(animators);
- } else {
- mGuidanceStylist.onImeDisappearing(animators);
- mActionsStylist.onImeDisappearing(animators);
- mButtonActionsStylist.onImeDisappearing(animators);
- }
- AnimatorSet set = new AnimatorSet();
- set.playTogether(animators);
- set.start();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
deleted file mode 100644
index dd037d2..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
+++ /dev/null
@@ -1,303 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from HeadersSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DividerPresenter;
-import android.support.v17.leanback.widget.DividerRow;
-import android.support.v17.leanback.widget.FocusHighlightHelper;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowHeaderPresenter;
-import android.support.v17.leanback.widget.SectionRow;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-/**
- * An fragment containing a list of row headers. Implementation must support three types of rows:
- * <ul>
- * <li>{@link DividerRow} rendered by {@link DividerPresenter}.</li>
- * <li>{@link Row} rendered by {@link RowHeaderPresenter}.</li>
- * <li>{@link SectionRow} rendered by {@link RowHeaderPresenter}.</li>
- * </ul>
- * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize
- * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}.
- */
-public class HeadersFragment extends BaseRowFragment {
-
- /**
- * Interface definition for a callback to be invoked when a header item is clicked.
- */
- public interface OnHeaderClickedListener {
- /**
- * Called when a header item has been clicked.
- *
- * @param viewHolder Row ViewHolder object corresponding to the selected Header.
- * @param row Row object corresponding to the selected Header.
- */
- void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row);
- }
-
- /**
- * Interface definition for a callback to be invoked when a header item is selected.
- */
- public interface OnHeaderViewSelectedListener {
- /**
- * Called when a header item has been selected.
- *
- * @param viewHolder Row ViewHolder object corresponding to the selected Header.
- * @param row Row object corresponding to the selected Header.
- */
- void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row);
- }
-
- private OnHeaderViewSelectedListener mOnHeaderViewSelectedListener;
- OnHeaderClickedListener mOnHeaderClickedListener;
- private boolean mHeadersEnabled = true;
- private boolean mHeadersGone = false;
- private int mBackgroundColor;
- private boolean mBackgroundColorSet;
-
- private static final PresenterSelector sHeaderPresenter = new ClassPresenterSelector()
- .addClassPresenter(DividerRow.class, new DividerPresenter())
- .addClassPresenter(SectionRow.class,
- new RowHeaderPresenter(R.layout.lb_section_header, false))
- .addClassPresenter(Row.class, new RowHeaderPresenter(R.layout.lb_header));
-
- public HeadersFragment() {
- setPresenterSelector(sHeaderPresenter);
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter());
- }
-
- public void setOnHeaderClickedListener(OnHeaderClickedListener listener) {
- mOnHeaderClickedListener = listener;
- }
-
- public void setOnHeaderViewSelectedListener(OnHeaderViewSelectedListener listener) {
- mOnHeaderViewSelectedListener = listener;
- }
-
- @Override
- VerticalGridView findGridViewFromRoot(View view) {
- return (VerticalGridView) view.findViewById(R.id.browse_headers);
- }
-
- @Override
- void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,
- int position, int subposition) {
- if (mOnHeaderViewSelectedListener != null) {
- if (viewHolder != null && position >= 0) {
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) viewHolder;
- mOnHeaderViewSelectedListener.onHeaderSelected(
- (RowHeaderPresenter.ViewHolder) vh.getViewHolder(), (Row) vh.getItem());
- } else {
- mOnHeaderViewSelectedListener.onHeaderSelected(null, null);
- }
- }
- }
-
- private final ItemBridgeAdapter.AdapterListener mAdapterListener =
- new ItemBridgeAdapter.AdapterListener() {
- @Override
- public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
- View headerView = viewHolder.getViewHolder().view;
- headerView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mOnHeaderClickedListener != null) {
- mOnHeaderClickedListener.onHeaderClicked(
- (RowHeaderPresenter.ViewHolder) viewHolder.getViewHolder(),
- (Row) viewHolder.getItem());
- }
- }
- });
- if (mWrapper != null) {
- viewHolder.itemView.addOnLayoutChangeListener(sLayoutChangeListener);
- } else {
- headerView.addOnLayoutChangeListener(sLayoutChangeListener);
- }
- }
-
- };
-
- static OnLayoutChangeListener sLayoutChangeListener = new OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- v.setPivotX(v.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? v.getWidth() : 0);
- v.setPivotY(v.getMeasuredHeight() / 2);
- }
- };
-
- @Override
- int getLayoutResourceId() {
- return R.layout.lb_headers_fragment;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- final VerticalGridView listView = getVerticalGridView();
- if (listView == null) {
- return;
- }
- if (mBackgroundColorSet) {
- listView.setBackgroundColor(mBackgroundColor);
- updateFadingEdgeToBrandColor(mBackgroundColor);
- } else {
- Drawable d = listView.getBackground();
- if (d instanceof ColorDrawable) {
- updateFadingEdgeToBrandColor(((ColorDrawable) d).getColor());
- }
- }
- updateListViewVisibility();
- }
-
- private void updateListViewVisibility() {
- final VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- getView().setVisibility(mHeadersGone ? View.GONE : View.VISIBLE);
- if (!mHeadersGone) {
- if (mHeadersEnabled) {
- listView.setChildrenVisibility(View.VISIBLE);
- } else {
- listView.setChildrenVisibility(View.INVISIBLE);
- }
- }
- }
- }
-
- void setHeadersEnabled(boolean enabled) {
- mHeadersEnabled = enabled;
- updateListViewVisibility();
- }
-
- void setHeadersGone(boolean gone) {
- mHeadersGone = gone;
- updateListViewVisibility();
- }
-
- static class NoOverlappingFrameLayout extends FrameLayout {
-
- public NoOverlappingFrameLayout(Context context) {
- super(context);
- }
-
- /**
- * Avoid creating hardware layer for header dock.
- */
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
- }
-
- // Wrapper needed because of conflict between RecyclerView's use of alpha
- // for ADD animations, and RowHeaderPresenter's use of alpha for selected level.
- final ItemBridgeAdapter.Wrapper mWrapper = new ItemBridgeAdapter.Wrapper() {
- @Override
- public void wrap(View wrapper, View wrapped) {
- ((FrameLayout) wrapper).addView(wrapped);
- }
-
- @Override
- public View createWrapper(View root) {
- return new NoOverlappingFrameLayout(root.getContext());
- }
- };
- @Override
- void updateAdapter() {
- super.updateAdapter();
- ItemBridgeAdapter adapter = getBridgeAdapter();
- adapter.setAdapterListener(mAdapterListener);
- adapter.setWrapper(mWrapper);
- }
-
- void setBackgroundColor(int color) {
- mBackgroundColor = color;
- mBackgroundColorSet = true;
-
- if (getVerticalGridView() != null) {
- getVerticalGridView().setBackgroundColor(mBackgroundColor);
- updateFadingEdgeToBrandColor(mBackgroundColor);
- }
- }
-
- private void updateFadingEdgeToBrandColor(int backgroundColor) {
- View fadingView = getView().findViewById(R.id.fade_out_edge);
- Drawable background = fadingView.getBackground();
- if (background instanceof GradientDrawable) {
- background.mutate();
- ((GradientDrawable) background).setColors(
- new int[] {Color.TRANSPARENT, backgroundColor});
- }
- }
-
- @Override
- public void onTransitionStart() {
- super.onTransitionStart();
- if (!mHeadersEnabled) {
- // When enabling headers fragment, the RowHeaderView gets a focus but
- // isShown() is still false because its parent is INVISIBLE, accessibility
- // event is not sent.
- // Workaround is: prevent focus to a child view during transition and put
- // focus on it after transition is done.
- final VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
- if (listView.hasFocus()) {
- listView.requestFocus();
- }
- }
- }
- }
-
- @Override
- public void onTransitionEnd() {
- if (mHeadersEnabled) {
- final VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
- if (listView.hasFocus()) {
- listView.requestFocus();
- }
- }
- }
- super.onTransitionEnd();
- }
-
- public boolean isScrolling() {
- return getVerticalGridView().getScrollState()
- != HorizontalGridView.SCROLL_STATE_IDLE;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
deleted file mode 100644
index f9af12f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package android.support.v17.leanback.app;
-
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.Row;
-
-/**
- * Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize
- * {@link RowsFragment}. We use invisible rows to represent
- * {@link android.support.v17.leanback.widget.DividerRow},
- * {@link android.support.v17.leanback.widget.SectionRow} and
- * {@link android.support.v17.leanback.widget.PageRow} in RowsFragment. In case we have an
- * invisible row at the end of a RowsFragment, it creates a jumping effect as the layout manager
- * thinks there are items even though they're invisible. This class takes care of filtering out
- * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the
- * bounds to reflect the latest data.
- */
-class ListRowDataAdapter extends ObjectAdapter {
- public static final int ON_ITEM_RANGE_CHANGED = 2;
- public static final int ON_ITEM_RANGE_INSERTED = 4;
- public static final int ON_ITEM_RANGE_REMOVED = 8;
- public static final int ON_CHANGED = 16;
-
- private final ObjectAdapter mAdapter;
- int mLastVisibleRowIndex;
-
- public ListRowDataAdapter(ObjectAdapter adapter) {
- super(adapter.getPresenterSelector());
- this.mAdapter = adapter;
- initialize();
-
- // If an user implements its own ObjectAdapter, notification corresponding to data
- // updates can be batched e.g. remove, add might be followed by notifyRemove, notifyAdd.
- // But underlying data would have changed during the notifyRemove call by the previous add
- // operation. To handle this case, we use QueueBasedDataObserver which forces
- // recyclerview to do a full data refresh after each update operation.
- if (adapter.isImmediateNotifySupported()) {
- mAdapter.registerObserver(new SimpleDataObserver());
- } else {
- mAdapter.registerObserver(new QueueBasedDataObserver());
- }
- }
-
- void initialize() {
- mLastVisibleRowIndex = -1;
- int i = mAdapter.size() - 1;
- while (i >= 0) {
- Row item = (Row) mAdapter.get(i);
- if (item.isRenderedAsRowView()) {
- mLastVisibleRowIndex = i;
- break;
- }
- i--;
- }
- }
-
- @Override
- public int size() {
- return mLastVisibleRowIndex + 1;
- }
-
- @Override
- public Object get(int index) {
- return mAdapter.get(index);
- }
-
- void doNotify(int eventType, int positionStart, int itemCount) {
- switch (eventType) {
- case ON_ITEM_RANGE_CHANGED:
- notifyItemRangeChanged(positionStart, itemCount);
- break;
- case ON_ITEM_RANGE_INSERTED:
- notifyItemRangeInserted(positionStart, itemCount);
- break;
- case ON_ITEM_RANGE_REMOVED:
- notifyItemRangeRemoved(positionStart, itemCount);
- break;
- case ON_CHANGED:
- notifyChanged();
- break;
- default:
- throw new IllegalArgumentException("Invalid event type " + eventType);
- }
- }
-
- private class SimpleDataObserver extends DataObserver {
-
- SimpleDataObserver() {
- }
-
- @Override
- public void onItemRangeChanged(int positionStart, int itemCount) {
- if (positionStart <= mLastVisibleRowIndex) {
- onEventFired(ON_ITEM_RANGE_CHANGED, positionStart,
- Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1));
- }
- }
-
- @Override
- public void onItemRangeInserted(int positionStart, int itemCount) {
- if (positionStart <= mLastVisibleRowIndex) {
- mLastVisibleRowIndex += itemCount;
- onEventFired(ON_ITEM_RANGE_INSERTED, positionStart, itemCount);
- return;
- }
-
- int lastVisibleRowIndex = mLastVisibleRowIndex;
- initialize();
- if (mLastVisibleRowIndex > lastVisibleRowIndex) {
- int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex;
- onEventFired(ON_ITEM_RANGE_INSERTED, lastVisibleRowIndex + 1, totalItems);
- }
- }
-
- @Override
- public void onItemRangeRemoved(int positionStart, int itemCount) {
- if (positionStart + itemCount - 1 < mLastVisibleRowIndex) {
- mLastVisibleRowIndex -= itemCount;
- onEventFired(ON_ITEM_RANGE_REMOVED, positionStart, itemCount);
- return;
- }
-
- int lastVisibleRowIndex = mLastVisibleRowIndex;
- initialize();
- int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
- if (totalItems > 0) {
- onEventFired(ON_ITEM_RANGE_REMOVED,
- Math.min(mLastVisibleRowIndex + 1, positionStart),
- totalItems);
- }
- }
-
- @Override
- public void onChanged() {
- initialize();
- onEventFired(ON_CHANGED, -1, -1);
- }
-
- protected void onEventFired(int eventType, int positionStart, int itemCount) {
- doNotify(eventType, positionStart, itemCount);
- }
- }
-
-
- /**
- * When using custom {@link ObjectAdapter}, it's possible that the user may make multiple
- * changes to the underlying data at once. The notifications about those updates may be
- * batched and the underlying data would have changed to reflect latest updates as opposed
- * to intermediate changes. In order to force RecyclerView to refresh the view with access
- * only to the final data, we call notifyChange().
- */
- private class QueueBasedDataObserver extends DataObserver {
-
- QueueBasedDataObserver() {
- }
-
- @Override
- public void onChanged() {
- initialize();
- notifyChanged();
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java b/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
deleted file mode 100644
index b69d5a7..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
+++ /dev/null
@@ -1,1025 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from OnboardingSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * 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 android.support.v17.leanback.app;
-
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.PagingIndicator;
-import android.app.Fragment;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnKeyListener;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * An OnboardingFragment provides a common and simple way to build onboarding screen for
- * applications.
- * <p>
- * <h3>Building the screen</h3>
- * The view structure of onboarding screen is composed of the common parts and custom parts. The
- * common parts are composed of icon, title, description and page navigator and the custom parts
- * are composed of background, contents and foreground.
- * <p>
- * To build the screen views, the inherited class should override:
- * <ul>
- * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same
- * size as the screen and the lowest z-order.</li>
- * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in
- * the content area at the center of the screen.</li>
- * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same
- * size as the screen and the highest z-order</li>
- * </ul>
- * <p>
- * Each of these methods can return {@code null} if the application doesn't want to provide it.
- * <p>
- * <h3>Page information</h3>
- * The onboarding screen may have several pages which explain the functionality of the application.
- * The inherited class should provide the page information by overriding the methods:
- * <p>
- * <ul>
- * <li>{@link #getPageCount} to provide the number of pages.</li>
- * <li>{@link #getPageTitle} to provide the title of the page.</li>
- * <li>{@link #getPageDescription} to provide the description of the page.</li>
- * </ul>
- * <p>
- * Note that the information is used in {@link #onCreateView}, so should be initialized before
- * calling {@code super.onCreateView}.
- * <p>
- * <h3>Animation</h3>
- * Onboarding screen has three kinds of animations:
- * <p>
- * <h4>Logo Splash Animation</a></h4>
- * When onboarding screen appears, the logo splash animation is played by default. The animation
- * fades in the logo image, pauses in a few seconds and fades it out.
- * <p>
- * In most cases, the logo animation needs to be customized because the logo images of applications
- * are different from each other, or some applications may want to show their own animations.
- * <p>
- * The logo animation can be customized in two ways:
- * <ul>
- * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show
- * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>
- * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the
- * {@link Animator} object to run.</li>
- * </ul>
- * <p>
- * If the inherited class provides neither the logo image nor the animation, the logo animation will
- * be omitted.
- * <h4>Page enter animation</h4>
- * After logo animation finishes, page enter animation starts, which causes the header section -
- * title and description views to fade and slide in. Users can override the default
- * fade + slide animation by overriding {@link #onCreateTitleAnimator()} &
- * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users
- * can provide animation by overriding {@link #onCreateEnterAnimation}.
- *
- * <h4>Page change animation</h4>
- * When the page changes, the default animations of the title and description are played. The
- * inherited class can override {@link #onPageChanged} to start the custom animations.
- * <p>
- * <h3>Finishing the screen</h3>
- * <p>
- * If the user finishes the onboarding screen after navigating all the pages,
- * {@link #onFinishFragment} is called. The inherited class can override this method to show another
- * fragment or activity, or just remove this fragment.
- * <p>
- * <h3>Theming</h3>
- * <p>
- * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must
- * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.
- * Themes can be provided in one of three ways:
- * <ul>
- * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme
- * that derives from it.</li>
- * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
- * existing Activity theme can have an entry added for the attribute
- * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used
- * by OnboardingFragment as an overlay to the Activity's theme.</li>
- * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the
- * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple
- * Activities.</li>
- * </ul>
- * <p>
- * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
- * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not
- * need to set the onboardingTheme attribute; if set, it will be ignored.)
- *
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
- * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
- */
-abstract public class OnboardingFragment extends Fragment {
- private static final String TAG = "OnboardingF";
- private static final boolean DEBUG = false;
-
- private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;
-
- private static final long HEADER_ANIMATION_DURATION_MS = 417;
- private static final long DESCRIPTION_START_DELAY_MS = 33;
- private static final long HEADER_APPEAR_DELAY_MS = 500;
- private static final int SLIDE_DISTANCE = 60;
-
- private static int sSlideDistance;
-
- private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();
- private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR =
- new AccelerateInterpolator();
-
- // Keys used to save and restore the states.
- private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index";
- private static final String KEY_LOGO_ANIMATION_FINISHED =
- "leanback.onboarding.logo_animation_finished";
- private static final String KEY_ENTER_ANIMATION_FINISHED =
- "leanback.onboarding.enter_animation_finished";
-
- private ContextThemeWrapper mThemeWrapper;
-
- PagingIndicator mPageIndicator;
- View mStartButton;
- private ImageView mLogoView;
- // Optional icon that can be displayed on top of the header section.
- private ImageView mMainIconView;
- private int mIconResourceId;
-
- TextView mTitleView;
- TextView mDescriptionView;
-
- boolean mIsLtr;
-
- // No need to save/restore the logo resource ID, because the logo animation will not appear when
- // the fragment is restored.
- private int mLogoResourceId;
- boolean mLogoAnimationFinished;
- boolean mEnterAnimationFinished;
- int mCurrentPageIndex;
-
- @ColorInt
- private int mTitleViewTextColor = Color.TRANSPARENT;
- private boolean mTitleViewTextColorSet;
-
- @ColorInt
- private int mDescriptionViewTextColor = Color.TRANSPARENT;
- private boolean mDescriptionViewTextColorSet;
-
- @ColorInt
- private int mDotBackgroundColor = Color.TRANSPARENT;
- private boolean mDotBackgroundColorSet;
-
- @ColorInt
- private int mArrowColor = Color.TRANSPARENT;
- private boolean mArrowColorSet;
-
- @ColorInt
- private int mArrowBackgroundColor = Color.TRANSPARENT;
- private boolean mArrowBackgroundColorSet;
-
- private CharSequence mStartButtonText;
- private boolean mStartButtonTextSet;
-
-
- private AnimatorSet mAnimator;
-
- private final OnClickListener mOnClickListener = new OnClickListener() {
- @Override
- public void onClick(View view) {
- if (!mLogoAnimationFinished) {
- // Do not change page until the enter transition finishes.
- return;
- }
- if (mCurrentPageIndex == getPageCount() - 1) {
- onFinishFragment();
- } else {
- moveToNextPage();
- }
- }
- };
-
- private final OnKeyListener mOnKeyListener = new OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (!mLogoAnimationFinished) {
- // Ignore key event until the enter transition finishes.
- return keyCode != KeyEvent.KEYCODE_BACK;
- }
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- return false;
- }
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- if (mCurrentPageIndex == 0) {
- return false;
- }
- moveToPreviousPage();
- return true;
- case KeyEvent.KEYCODE_DPAD_LEFT:
- if (mIsLtr) {
- moveToPreviousPage();
- } else {
- moveToNextPage();
- }
- return true;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- if (mIsLtr) {
- moveToNextPage();
- } else {
- moveToPreviousPage();
- }
- return true;
- }
- return false;
- }
- };
-
- /**
- * Navigates to the previous page.
- */
- protected void moveToPreviousPage() {
- if (!mLogoAnimationFinished) {
- // Ignore if the logo enter transition is in progress.
- return;
- }
- if (mCurrentPageIndex > 0) {
- --mCurrentPageIndex;
- onPageChangedInternal(mCurrentPageIndex + 1);
- }
- }
-
- /**
- * Navigates to the next page.
- */
- protected void moveToNextPage() {
- if (!mLogoAnimationFinished) {
- // Ignore if the logo enter transition is in progress.
- return;
- }
- if (mCurrentPageIndex < getPageCount() - 1) {
- ++mCurrentPageIndex;
- onPageChangedInternal(mCurrentPageIndex - 1);
- }
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, final ViewGroup container,
- Bundle savedInstanceState) {
- resolveTheme();
- LayoutInflater localInflater = getThemeInflater(inflater);
- final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
- container, false);
- mIsLtr = getResources().getConfiguration().getLayoutDirection()
- == View.LAYOUT_DIRECTION_LTR;
- mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
- mPageIndicator.setOnClickListener(mOnClickListener);
- mPageIndicator.setOnKeyListener(mOnKeyListener);
- mStartButton = view.findViewById(R.id.button_start);
- mStartButton.setOnClickListener(mOnClickListener);
- mStartButton.setOnKeyListener(mOnKeyListener);
- mMainIconView = (ImageView) view.findViewById(R.id.main_icon);
- mLogoView = (ImageView) view.findViewById(R.id.logo);
- mTitleView = (TextView) view.findViewById(R.id.title);
- mDescriptionView = (TextView) view.findViewById(R.id.description);
-
- if (mTitleViewTextColorSet) {
- mTitleView.setTextColor(mTitleViewTextColor);
- }
- if (mDescriptionViewTextColorSet) {
- mDescriptionView.setTextColor(mDescriptionViewTextColor);
- }
- if (mDotBackgroundColorSet) {
- mPageIndicator.setDotBackgroundColor(mDotBackgroundColor);
- }
- if (mArrowColorSet) {
- mPageIndicator.setArrowColor(mArrowColor);
- }
- if (mArrowBackgroundColorSet) {
- mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor);
- }
- if (mStartButtonTextSet) {
- ((Button) mStartButton).setText(mStartButtonText);
- }
- final Context context = FragmentUtil.getContext(OnboardingFragment.this);
- if (sSlideDistance == 0) {
- sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
- .getDisplayMetrics().scaledDensity);
- }
- view.requestFocus();
- return view;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- if (savedInstanceState == null) {
- mCurrentPageIndex = 0;
- mLogoAnimationFinished = false;
- mEnterAnimationFinished = false;
- mPageIndicator.onPageSelected(0, false);
- view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- getView().getViewTreeObserver().removeOnPreDrawListener(this);
- if (!startLogoAnimation()) {
- mLogoAnimationFinished = true;
- onLogoAnimationFinished();
- }
- return true;
- }
- });
- } else {
- mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);
- mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED);
- mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED);
- if (!mLogoAnimationFinished) {
- // logo animation wasn't started or was interrupted when the activity was destroyed;
- // restart it againl
- if (!startLogoAnimation()) {
- mLogoAnimationFinished = true;
- onLogoAnimationFinished();
- }
- } else {
- onLogoAnimationFinished();
- }
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);
- outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished);
- outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished);
- }
-
- /**
- * Sets the text color for TitleView. If not set, the default textColor set in style
- * referenced by attr {@link R.attr#onboardingTitleStyle} will be used.
- * @param color the color to use as the text color for TitleView
- */
- public void setTitleViewTextColor(@ColorInt int color) {
- mTitleViewTextColor = color;
- mTitleViewTextColorSet = true;
- if (mTitleView != null) {
- mTitleView.setTextColor(color);
- }
- }
-
- /**
- * Returns the text color of TitleView if it's set through
- * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.
- */
- @ColorInt
- public final int getTitleViewTextColor() {
- return mTitleViewTextColor;
- }
-
- /**
- * Sets the text color for DescriptionView. If not set, the default textColor set in style
- * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used.
- * @param color the color to use as the text color for DescriptionView
- */
- public void setDescriptionViewTextColor(@ColorInt int color) {
- mDescriptionViewTextColor = color;
- mDescriptionViewTextColorSet = true;
- if (mDescriptionView != null) {
- mDescriptionView.setTextColor(color);
- }
- }
-
- /**
- * Returns the text color of DescriptionView if it's set through
- * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.
- */
- @ColorInt
- public final int getDescriptionViewTextColor() {
- return mDescriptionViewTextColor;
- }
- /**
- * Sets the background color of the dots. If not set, the default color from attr
- * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used.
- * @param color the color to use for dot backgrounds
- */
- public void setDotBackgroundColor(@ColorInt int color) {
- mDotBackgroundColor = color;
- mDotBackgroundColorSet = true;
- if (mPageIndicator != null) {
- mPageIndicator.setDotBackgroundColor(color);
- }
- }
-
- /**
- * Returns the background color of the dot if it's set through
- * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.
- */
- @ColorInt
- public final int getDotBackgroundColor() {
- return mDotBackgroundColor;
- }
-
- /**
- * Sets the color of the arrow. This color will supersede the color set in the theme attribute
- * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the
- * arrow will have its original bitmap color.
- *
- * @param color the color to use for arrow background
- */
- public void setArrowColor(@ColorInt int color) {
- mArrowColor = color;
- mArrowColorSet = true;
- if (mPageIndicator != null) {
- mPageIndicator.setArrowColor(color);
- }
- }
-
- /**
- * Returns the color of the arrow if it's set through
- * {@link #setArrowColor(int)}. If no color was set, transparent is returned.
- */
- @ColorInt
- public final int getArrowColor() {
- return mArrowColor;
- }
-
- /**
- * Sets the background color of the arrow. If not set, the default color from attr
- * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used.
- * @param color the color to use for arrow background
- */
- public void setArrowBackgroundColor(@ColorInt int color) {
- mArrowBackgroundColor = color;
- mArrowBackgroundColorSet = true;
- if (mPageIndicator != null) {
- mPageIndicator.setArrowBackgroundColor(color);
- }
- }
-
- /**
- * Returns the background color of the arrow if it's set through
- * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.
- */
- @ColorInt
- public final int getArrowBackgroundColor() {
- return mArrowBackgroundColor;
- }
-
- /**
- * Returns the start button text if it's set through
- * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
- */
- public final CharSequence getStartButtonText() {
- return mStartButtonText;
- }
-
- /**
- * Sets the text on the start button text. If not set, the default text set in
- * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used.
- *
- * @param text the start button text
- */
- public void setStartButtonText(CharSequence text) {
- mStartButtonText = text;
- mStartButtonTextSet = true;
- if (mStartButton != null) {
- ((Button) mStartButton).setText(mStartButtonText);
- }
- }
-
- /**
- * Returns the theme used for styling the fragment. The default returns -1, indicating that the
- * host Activity's theme should be used.
- *
- * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host
- * Activity's theme.
- */
- public int onProvideTheme() {
- return -1;
- }
-
- private void resolveTheme() {
- final Context context = FragmentUtil.getContext(OnboardingFragment.this);
- int theme = onProvideTheme();
- if (theme == -1) {
- // Look up the onboardingTheme in the activity's currently specified theme. If it
- // exists, wrap the theme with its value.
- int resId = R.attr.onboardingTheme;
- TypedValue typedValue = new TypedValue();
- boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
- if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
- if (found) {
- mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
- }
- } else {
- mThemeWrapper = new ContextThemeWrapper(context, theme);
- }
- }
-
- private LayoutInflater getThemeInflater(LayoutInflater inflater) {
- return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);
- }
-
- /**
- * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo
- * splash animation will be played.
- *
- * @param id The resource ID of the logo image.
- */
- public final void setLogoResourceId(int id) {
- mLogoResourceId = id;
- }
-
- /**
- * Returns the resource ID of the splash logo image.
- *
- * @return The resource ID of the splash logo image.
- */
- public final int getLogoResourceId() {
- return mLogoResourceId;
- }
-
- /**
- * Called to have the inherited class create its own logo animation.
- * <p>
- * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.
- * If this returns {@code null}, the logo animation is skipped.
- *
- * @return The {@link Animator} object which runs the logo animation.
- */
- @Nullable
- protected Animator onCreateLogoAnimation() {
- return null;
- }
-
- boolean startLogoAnimation() {
- final Context context = FragmentUtil.getContext(OnboardingFragment.this);
- if (context == null) {
- return false;
- }
- Animator animator = null;
- if (mLogoResourceId != 0) {
- mLogoView.setVisibility(View.VISIBLE);
- mLogoView.setImageResource(mLogoResourceId);
- Animator inAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_logo_enter);
- Animator outAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_logo_exit);
- outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
- AnimatorSet logoAnimator = new AnimatorSet();
- logoAnimator.playSequentially(inAnimator, outAnimator);
- logoAnimator.setTarget(mLogoView);
- animator = logoAnimator;
- } else {
- animator = onCreateLogoAnimation();
- }
- if (animator != null) {
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (context != null) {
- mLogoAnimationFinished = true;
- onLogoAnimationFinished();
- }
- }
- });
- animator.start();
- return true;
- }
- return false;
- }
-
- /**
- * Called to have the inherited class create its enter animation. The start animation runs after
- * logo animation ends.
- *
- * @return The {@link Animator} object which runs the page enter animation.
- */
- @Nullable
- protected Animator onCreateEnterAnimation() {
- return null;
- }
-
-
- /**
- * Hides the logo view and makes other fragment views visible. Also initializes the texts for
- * Title and Description views.
- */
- void hideLogoView() {
- mLogoView.setVisibility(View.GONE);
-
- if (mIconResourceId != 0) {
- mMainIconView.setImageResource(mIconResourceId);
- mMainIconView.setVisibility(View.VISIBLE);
- }
-
- View container = getView();
- // Create custom views.
- LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
- FragmentUtil.getContext(OnboardingFragment.this)));
- ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
- R.id.background_container);
- View background = onCreateBackgroundView(inflater, backgroundContainer);
- if (background != null) {
- backgroundContainer.setVisibility(View.VISIBLE);
- backgroundContainer.addView(background);
- }
- ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);
- View content = onCreateContentView(inflater, contentContainer);
- if (content != null) {
- contentContainer.setVisibility(View.VISIBLE);
- contentContainer.addView(content);
- }
- ViewGroup foregroundContainer = (ViewGroup) container.findViewById(
- R.id.foreground_container);
- View foreground = onCreateForegroundView(inflater, foregroundContainer);
- if (foreground != null) {
- foregroundContainer.setVisibility(View.VISIBLE);
- foregroundContainer.addView(foreground);
- }
- // Make views visible which were invisible while logo animation is running.
- container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);
- container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);
- if (getPageCount() > 1) {
- mPageIndicator.setPageCount(getPageCount());
- mPageIndicator.onPageSelected(mCurrentPageIndex, false);
- }
- if (mCurrentPageIndex == getPageCount() - 1) {
- mStartButton.setVisibility(View.VISIBLE);
- } else {
- mPageIndicator.setVisibility(View.VISIBLE);
- }
- // Header views.
- mTitleView.setText(getPageTitle(mCurrentPageIndex));
- mDescriptionView.setText(getPageDescription(mCurrentPageIndex));
- }
-
- /**
- * Called immediately after the logo animation is complete or no logo animation is specified.
- * This method can also be called when the activity is recreated, i.e. when no logo animation
- * are performed.
- * By default, this method will hide the logo view and start the entrance animation for this
- * fragment.
- * Overriding subclasses can provide their own data loading logic as to when the entrance
- * animation should be executed.
- */
- protected void onLogoAnimationFinished() {
- startEnterAnimation(false);
- }
-
- /**
- * Called to start entrance transition. This can be called by subclasses when the logo animation
- * and data loading is complete. If force flag is set to false, it will only start the animation
- * if it's not already done yet. Otherwise, it will always start the enter animation. In both
- * cases, the logo view will hide and the rest of fragment views become visible after this call.
- *
- * @param force {@code true} if enter animation has to be performed regardless of whether it's
- * been done in the past, {@code false} otherwise
- */
- protected final void startEnterAnimation(boolean force) {
- final Context context = FragmentUtil.getContext(OnboardingFragment.this);
- if (context == null) {
- return;
- }
- hideLogoView();
- if (mEnterAnimationFinished && !force) {
- return;
- }
- List<Animator> animators = new ArrayList<>();
- Animator animator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_page_indicator_enter);
- animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
- animators.add(animator);
-
- animator = onCreateTitleAnimator();
- if (animator != null) {
- // Header title.
- animator.setTarget(mTitleView);
- animators.add(animator);
- }
-
- animator = onCreateDescriptionAnimator();
- if (animator != null) {
- // Header description.
- animator.setTarget(mDescriptionView);
- animators.add(animator);
- }
-
- // Customized animation by the inherited class.
- Animator customAnimator = onCreateEnterAnimation();
- if (customAnimator != null) {
- animators.add(customAnimator);
- }
-
- // Return if we don't have any animations.
- if (animators.isEmpty()) {
- return;
- }
- mAnimator = new AnimatorSet();
- mAnimator.playTogether(animators);
- mAnimator.start();
- mAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mEnterAnimationFinished = true;
- }
- });
- // Search focus and give the focus to the appropriate child which has become visible.
- getView().requestFocus();
- }
-
- /**
- * Provides the entry animation for description view. This allows users to override the
- * default fade and slide animation. Returning null will disable the animation.
- */
- protected Animator onCreateDescriptionAnimator() {
- return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
- R.animator.lb_onboarding_description_enter);
- }
-
- /**
- * Provides the entry animation for title view. This allows users to override the
- * default fade and slide animation. Returning null will disable the animation.
- */
- protected Animator onCreateTitleAnimator() {
- return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
- R.animator.lb_onboarding_title_enter);
- }
-
- /**
- * Returns whether the logo enter animation is finished.
- *
- * @return {@code true} if the logo enter transition is finished, {@code false} otherwise
- */
- protected final boolean isLogoAnimationFinished() {
- return mLogoAnimationFinished;
- }
-
- /**
- * Returns the page count.
- *
- * @return The page count.
- */
- abstract protected int getPageCount();
-
- /**
- * Returns the title of the given page.
- *
- * @param pageIndex The page index.
- *
- * @return The title of the page.
- */
- abstract protected CharSequence getPageTitle(int pageIndex);
-
- /**
- * Returns the description of the given page.
- *
- * @param pageIndex The page index.
- *
- * @return The description of the page.
- */
- abstract protected CharSequence getPageDescription(int pageIndex);
-
- /**
- * Returns the index of the current page.
- *
- * @return The index of the current page.
- */
- protected final int getCurrentPageIndex() {
- return mCurrentPageIndex;
- }
-
- /**
- * Called to have the inherited class create background view. This is optional and the fragment
- * which doesn't have the background view can return {@code null}. This is called inside
- * {@link #onCreateView}.
- *
- * @param inflater The LayoutInflater object that can be used to inflate the views,
- * @param container The parent view that the additional views are attached to.The fragment
- * should not add the view by itself.
- *
- * @return The background view for the onboarding screen, or {@code null}.
- */
- @Nullable
- abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container);
-
- /**
- * Called to have the inherited class create content view. This is optional and the fragment
- * which doesn't have the content view can return {@code null}. This is called inside
- * {@link #onCreateView}.
- *
- * <p>The content view would be located at the center of the screen.
- *
- * @param inflater The LayoutInflater object that can be used to inflate the views,
- * @param container The parent view that the additional views are attached to.The fragment
- * should not add the view by itself.
- *
- * @return The content view for the onboarding screen, or {@code null}.
- */
- @Nullable
- abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container);
-
- /**
- * Called to have the inherited class create foreground view. This is optional and the fragment
- * which doesn't need the foreground view can return {@code null}. This is called inside
- * {@link #onCreateView}.
- *
- * <p>This foreground view would have the highest z-order.
- *
- * @param inflater The LayoutInflater object that can be used to inflate the views,
- * @param container The parent view that the additional views are attached to.The fragment
- * should not add the view by itself.
- *
- * @return The foreground view for the onboarding screen, or {@code null}.
- */
- @Nullable
- abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
-
- /**
- * Called when the onboarding flow finishes.
- */
- protected void onFinishFragment() { }
-
- /**
- * Called when the page changes.
- */
- private void onPageChangedInternal(int previousPage) {
- if (mAnimator != null) {
- mAnimator.end();
- }
- mPageIndicator.onPageSelected(mCurrentPageIndex, true);
-
- List<Animator> animators = new ArrayList<>();
- // Header animation
- Animator fadeAnimator = null;
- if (previousPage < getCurrentPageIndex()) {
- // sliding to left
- animators.add(createAnimator(mTitleView, false, Gravity.START, 0));
- animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,
- DESCRIPTION_START_DELAY_MS));
- animators.add(createAnimator(mTitleView, true, Gravity.END,
- HEADER_APPEAR_DELAY_MS));
- animators.add(createAnimator(mDescriptionView, true, Gravity.END,
- HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
- } else {
- // sliding to right
- animators.add(createAnimator(mTitleView, false, Gravity.END, 0));
- animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,
- DESCRIPTION_START_DELAY_MS));
- animators.add(createAnimator(mTitleView, true, Gravity.START,
- HEADER_APPEAR_DELAY_MS));
- animators.add(createAnimator(mDescriptionView, true, Gravity.START,
- HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
- }
- final int currentPageIndex = getCurrentPageIndex();
- fadeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mTitleView.setText(getPageTitle(currentPageIndex));
- mDescriptionView.setText(getPageDescription(currentPageIndex));
- }
- });
-
- final Context context = FragmentUtil.getContext(OnboardingFragment.this);
- // Animator for switching between page indicator and button.
- if (getCurrentPageIndex() == getPageCount() - 1) {
- mStartButton.setVisibility(View.VISIBLE);
- Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_page_indicator_fade_out);
- navigatorFadeOutAnimator.setTarget(mPageIndicator);
- navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mPageIndicator.setVisibility(View.GONE);
- }
- });
- animators.add(navigatorFadeOutAnimator);
- Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_start_button_fade_in);
- buttonFadeInAnimator.setTarget(mStartButton);
- animators.add(buttonFadeInAnimator);
- } else if (previousPage == getPageCount() - 1) {
- mPageIndicator.setVisibility(View.VISIBLE);
- Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_page_indicator_fade_in);
- navigatorFadeInAnimator.setTarget(mPageIndicator);
- animators.add(navigatorFadeInAnimator);
- Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
- R.animator.lb_onboarding_start_button_fade_out);
- buttonFadeOutAnimator.setTarget(mStartButton);
- buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mStartButton.setVisibility(View.GONE);
- }
- });
- animators.add(buttonFadeOutAnimator);
- }
- mAnimator = new AnimatorSet();
- mAnimator.playTogether(animators);
- mAnimator.start();
- onPageChanged(mCurrentPageIndex, previousPage);
- }
-
- /**
- * Called when the page has been changed.
- *
- * @param newPage The new page.
- * @param previousPage The previous page.
- */
- protected void onPageChanged(int newPage, int previousPage) { }
-
- private Animator createAnimator(View view, boolean fadeIn, int slideDirection,
- long startDelay) {
- boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
- boolean slideRight = (isLtr && slideDirection == Gravity.END)
- || (!isLtr && slideDirection == Gravity.START)
- || slideDirection == Gravity.RIGHT;
- Animator fadeAnimator;
- Animator slideAnimator;
- if (fadeIn) {
- fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);
- slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
- slideRight ? sSlideDistance : -sSlideDistance, 0);
- fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
- slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
- } else {
- fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);
- slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,
- slideRight ? sSlideDistance : -sSlideDistance);
- fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
- slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
- }
- fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
- fadeAnimator.setTarget(view);
- slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
- slideAnimator.setTarget(view);
- AnimatorSet animator = new AnimatorSet();
- animator.playTogether(fadeAnimator, slideAnimator);
- if (startDelay > 0) {
- animator.setStartDelay(startDelay);
- }
- return animator;
- }
-
- /**
- * Sets the resource id for the main icon.
- */
- public final void setIconResouceId(int resourceId) {
- this.mIconResourceId = resourceId;
- if (mMainIconView != null) {
- mMainIconView.setImageResource(resourceId);
- mMainIconView.setVisibility(View.VISIBLE);
- }
- }
-
- /**
- * Returns the resource id of the main icon.
- */
- public final int getIconResourceId() {
- return mIconResourceId;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
deleted file mode 100644
index 33e787c..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ /dev/null
@@ -1,1174 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from PlaybackSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.animation.LogAccelerateInterpolator;
-import android.support.v17.leanback.animation.LogDecelerateInterpolator;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
-import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.ItemAlignmentFacet;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.PlaybackRowPresenter;
-import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
-import android.support.v17.leanback.widget.PlaybackSeekUi;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.app.Fragment;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-
-/**
- * A fragment for displaying playback controls and related content.
- *
- * <p>
- * A PlaybackFragment renders the elements of its {@link ObjectAdapter} as a set
- * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
- * of {@link RowPresenter}.
- * </p>
- * <p>
- * A playback row is a row rendered by {@link PlaybackRowPresenter}.
- * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter.
- * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it.
- * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are
- * optional, app can pass playback row and PlaybackRowPresenter in the adapter using
- * {@link #setAdapter(ObjectAdapter)}.
- * </p>
- * <p>
- * Auto hide controls upon playing: best practice is calling
- * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
- * be cancelled upon {@link #tickle()} triggered by input event.
- * </p>
- */
-public class PlaybackFragment extends Fragment {
- static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
-
- /**
- * No background.
- */
- public static final int BG_NONE = 0;
-
- /**
- * A dark translucent background.
- */
- public static final int BG_DARK = 1;
- PlaybackGlueHost.HostCallback mHostCallback;
-
- PlaybackSeekUi.Client mSeekUiClient;
- boolean mInSeek;
- ProgressBarManager mProgressBarManager = new ProgressBarManager();
-
- /**
- * Resets the focus on the button in the middle of control row.
- * @hide
- */
- public void resetFocus() {
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
- .findViewHolderForAdapterPosition(0);
- if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
- ((PlaybackRowPresenter) vh.getPresenter()).onReappear(
- (RowPresenter.ViewHolder) vh.getViewHolder());
- }
- }
-
- private class SetSelectionRunnable implements Runnable {
- int mPosition;
- boolean mSmooth = true;
-
- @Override
- public void run() {
- if (mRowsFragment == null) {
- return;
- }
- mRowsFragment.setSelectedPosition(mPosition, mSmooth);
- }
- }
-
- /**
- * A light translucent background.
- */
- public static final int BG_LIGHT = 2;
- RowsFragment mRowsFragment;
- ObjectAdapter mAdapter;
- PlaybackRowPresenter mPresenter;
- Row mRow;
- BaseOnItemViewSelectedListener mExternalItemSelectedListener;
- BaseOnItemViewClickedListener mExternalItemClickedListener;
- BaseOnItemViewClickedListener mPlaybackItemClickedListener;
-
- private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
- new BaseOnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder,
- Object item,
- RowPresenter.ViewHolder rowViewHolder,
- Object row) {
- if (mPlaybackItemClickedListener != null
- && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
- mPlaybackItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- if (mExternalItemClickedListener != null) {
- mExternalItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- }
- };
-
- private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
- new BaseOnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder,
- Object item,
- RowPresenter.ViewHolder rowViewHolder,
- Object row) {
- if (mExternalItemSelectedListener != null) {
- mExternalItemSelectedListener.onItemSelected(
- itemViewHolder, item, rowViewHolder, row);
- }
- }
- };
-
- private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
-
- public ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- /**
- * Listener allowing the application to receive notification of fade in and/or fade out
- * completion events.
- * @hide
- */
- public static class OnFadeCompleteListener {
- public void onFadeInComplete() {
- }
-
- public void onFadeOutComplete() {
- }
- }
-
- private static final String TAG = "PlaybackFragment";
- private static final boolean DEBUG = false;
- private static final int ANIMATION_MULTIPLIER = 1;
-
- private static int START_FADE_OUT = 1;
-
- // Fading status
- private static final int IDLE = 0;
- private static final int ANIMATING = 1;
-
- int mPaddingBottom;
- int mOtherRowsCenterToBottom;
- View mRootView;
- View mBackgroundView;
- int mBackgroundType = BG_DARK;
- int mBgDarkColor;
- int mBgLightColor;
- int mShowTimeMs;
- int mMajorFadeTranslateY, mMinorFadeTranslateY;
- int mAnimationTranslateY;
- OnFadeCompleteListener mFadeCompleteListener;
- View.OnKeyListener mInputEventHandler;
- boolean mFadingEnabled = true;
- boolean mControlVisibleBeforeOnCreateView = true;
- boolean mControlVisible = true;
- int mBgAlpha;
- ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
- ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
- ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator;
-
- private final Animator.AnimatorListener mFadeListener =
- new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- enableVerticalGridAnimations(false);
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
- if (mBgAlpha > 0) {
- enableVerticalGridAnimations(true);
- if (mFadeCompleteListener != null) {
- mFadeCompleteListener.onFadeInComplete();
- }
- } else {
- VerticalGridView verticalView = getVerticalGridView();
- // reset focus to the primary actions only if the selected row was the controls row
- if (verticalView != null && verticalView.getSelectedPosition() == 0) {
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- verticalView.findViewHolderForAdapterPosition(0);
- if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
- ((PlaybackRowPresenter)vh.getPresenter()).onReappear(
- (RowPresenter.ViewHolder) vh.getViewHolder());
- }
- }
- if (mFadeCompleteListener != null) {
- mFadeCompleteListener.onFadeOutComplete();
- }
- }
- }
- };
-
- public PlaybackFragment() {
- mProgressBarManager.setInitialDelay(500);
- }
-
- VerticalGridView getVerticalGridView() {
- if (mRowsFragment == null) {
- return null;
- }
- return mRowsFragment.getVerticalGridView();
- }
-
- private final Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message message) {
- if (message.what == START_FADE_OUT && mFadingEnabled) {
- hideControlsOverlay(true);
- }
- }
- };
-
- private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener =
- new VerticalGridView.OnTouchInterceptListener() {
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- return onInterceptInputEvent(event);
- }
- };
-
- private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener =
- new VerticalGridView.OnKeyInterceptListener() {
- @Override
- public boolean onInterceptKeyEvent(KeyEvent event) {
- return onInterceptInputEvent(event);
- }
- };
-
- private void setBgAlpha(int alpha) {
- mBgAlpha = alpha;
- if (mBackgroundView != null) {
- mBackgroundView.getBackground().setAlpha(alpha);
- }
- }
-
- private void enableVerticalGridAnimations(boolean enable) {
- if (getVerticalGridView() != null) {
- getVerticalGridView().setAnimateChildLayout(enable);
- }
- }
-
- /**
- * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.
- * If enabled and fragment is resumed, the view will fade out after a time period.
- * {@link #tickle()} will kill the timer, next time fragment is resumed,
- * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.
- */
- public void setControlsOverlayAutoHideEnabled(boolean enabled) {
- if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled);
- if (enabled != mFadingEnabled) {
- mFadingEnabled = enabled;
- if (isResumed() && getView().hasFocus()) {
- showControlsOverlay(true);
- if (enabled) {
- // StateGraph 7->2 5->2
- startFadeTimer();
- } else {
- // StateGraph 4->5 2->5
- stopFadeTimer();
- }
- } else {
- // StateGraph 6->1 1->6
- }
- }
- }
-
- /**
- * Returns true if controls will be auto hidden after a delay when fragment is resumed.
- */
- public boolean isControlsOverlayAutoHideEnabled() {
- return mFadingEnabled;
- }
-
- /**
- * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}
- */
- @Deprecated
- public void setFadingEnabled(boolean enabled) {
- setControlsOverlayAutoHideEnabled(enabled);
- }
-
- /**
- * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}
- */
- @Deprecated
- public boolean isFadingEnabled() {
- return isControlsOverlayAutoHideEnabled();
- }
-
- /**
- * Sets the listener to be called when fade in or out has completed.
- * @hide
- */
- public void setFadeCompleteListener(OnFadeCompleteListener listener) {
- mFadeCompleteListener = listener;
- }
-
- /**
- * Returns the listener to be called when fade in or out has completed.
- * @hide
- */
- public OnFadeCompleteListener getFadeCompleteListener() {
- return mFadeCompleteListener;
- }
-
- /**
- * Sets the input event handler.
- */
- public final void setOnKeyInterceptListener(View.OnKeyListener handler) {
- mInputEventHandler = handler;
- }
-
- /**
- * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will
- * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When
- * next time fragment is resumed, the timer will be started again if
- * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call
- * this method, tickling on input events is handled by the fragment.
- */
- public void tickle() {
- if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
- //StateGraph 2->4
- stopFadeTimer();
- showControlsOverlay(true);
- }
-
- private boolean onInterceptInputEvent(InputEvent event) {
- final boolean controlsHidden = !mControlVisible;
- if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event);
- boolean consumeEvent = false;
- int keyCode = KeyEvent.KEYCODE_UNKNOWN;
- int keyAction = 0;
-
- if (event instanceof KeyEvent) {
- keyCode = ((KeyEvent) event).getKeyCode();
- keyAction = ((KeyEvent) event).getAction();
- if (mInputEventHandler != null) {
- consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);
- }
- }
-
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- // Event may be consumed; regardless, if controls are hidden then these keys will
- // bring up the controls.
- if (controlsHidden) {
- consumeEvent = true;
- }
- if (keyAction == KeyEvent.ACTION_DOWN) {
- tickle();
- }
- break;
- case KeyEvent.KEYCODE_BACK:
- case KeyEvent.KEYCODE_ESCAPE:
- if (mInSeek) {
- // when in seek, the SeekUi will handle the BACK.
- return false;
- }
- // If controls are not hidden, back will be consumed to fade
- // them out (even if the key was consumed by the handler).
- if (!controlsHidden) {
- consumeEvent = true;
-
- if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {
- hideControlsOverlay(true);
- }
- }
- break;
- default:
- if (consumeEvent) {
- if (keyAction == KeyEvent.ACTION_DOWN) {
- tickle();
- }
- }
- }
- return consumeEvent;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- // controls view are initially visible, make it invisible
- // if app has called hideControlsOverlay() before view created.
- mControlVisible = true;
- if (!mControlVisibleBeforeOnCreateView) {
- showControlsOverlay(false, false);
- mControlVisibleBeforeOnCreateView = true;
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (mControlVisible) {
- //StateGraph: 6->5 1->2
- if (mFadingEnabled) {
- // StateGraph 1->2
- startFadeTimer();
- }
- } else {
- //StateGraph: 6->7 1->3
- }
- getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
- getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
- if (mHostCallback != null) {
- mHostCallback.onHostResume();
- }
- }
-
- private void stopFadeTimer() {
- if (mHandler != null) {
- mHandler.removeMessages(START_FADE_OUT);
- }
- }
-
- private void startFadeTimer() {
- if (mHandler != null) {
- mHandler.removeMessages(START_FADE_OUT);
- mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs);
- }
- }
-
- private static ValueAnimator loadAnimator(Context context, int resId) {
- ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId);
- animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER);
- return animator;
- }
-
- private void loadBgAnimator() {
- AnimatorUpdateListener listener = new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator arg0) {
- setBgAlpha((Integer) arg0.getAnimatedValue());
- }
- };
-
- Context context = FragmentUtil.getContext(PlaybackFragment.this);
- mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
- mBgFadeInAnimator.addUpdateListener(listener);
- mBgFadeInAnimator.addListener(mFadeListener);
-
- mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
- mBgFadeOutAnimator.addUpdateListener(listener);
- mBgFadeOutAnimator.addListener(mFadeListener);
- }
-
- private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);
- private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);
-
- private void loadControlRowAnimator() {
- final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator arg0) {
- if (getVerticalGridView() == null) {
- return;
- }
- RecyclerView.ViewHolder vh = getVerticalGridView()
- .findViewHolderForAdapterPosition(0);
- if (vh == null) {
- return;
- }
- View view = vh.itemView;
- if (view != null) {
- final float fraction = (Float) arg0.getAnimatedValue();
- if (DEBUG) Log.v(TAG, "fraction " + fraction);
- view.setAlpha(fraction);
- view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
- }
- }
- };
-
- Context context = FragmentUtil.getContext(PlaybackFragment.this);
- mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
- mControlRowFadeInAnimator.addUpdateListener(updateListener);
- mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
-
- mControlRowFadeOutAnimator = loadAnimator(context,
- R.animator.lb_playback_controls_fade_out);
- mControlRowFadeOutAnimator.addUpdateListener(updateListener);
- mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
- }
-
- private void loadOtherRowAnimator() {
- final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator arg0) {
- if (getVerticalGridView() == null) {
- return;
- }
- final float fraction = (Float) arg0.getAnimatedValue();
- final int count = getVerticalGridView().getChildCount();
- for (int i = 0; i < count; i++) {
- View view = getVerticalGridView().getChildAt(i);
- if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
- view.setAlpha(fraction);
- view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
- }
- }
- }
- };
-
- Context context = FragmentUtil.getContext(PlaybackFragment.this);
- mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
- mOtherRowFadeInAnimator.addUpdateListener(updateListener);
- mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
-
- mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
- mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
- mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
- }
-
- /**
- * Fades out the playback overlay immediately.
- * @deprecated Call {@link #hideControlsOverlay(boolean)}
- */
- @Deprecated
- public void fadeOut() {
- showControlsOverlay(false, false);
- }
-
- /**
- * Show controls overlay.
- *
- * @param runAnimation True to run animation, false otherwise.
- */
- public void showControlsOverlay(boolean runAnimation) {
- showControlsOverlay(true, runAnimation);
- }
-
- /**
- * Returns true if controls overlay is visible, false otherwise.
- *
- * @return True if controls overlay is visible, false otherwise.
- * @see #showControlsOverlay(boolean)
- * @see #hideControlsOverlay(boolean)
- */
- public boolean isControlsOverlayVisible() {
- return mControlVisible;
- }
-
- /**
- * Hide controls overlay.
- *
- * @param runAnimation True to run animation, false otherwise.
- */
- public void hideControlsOverlay(boolean runAnimation) {
- showControlsOverlay(false, runAnimation);
- }
-
- /**
- * if first animator is still running, reverse it; otherwise start second animator.
- */
- static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,
- boolean runAnimation) {
- if (first.isStarted()) {
- first.reverse();
- if (!runAnimation) {
- first.end();
- }
- } else {
- second.start();
- if (!runAnimation) {
- second.end();
- }
- }
- }
-
- /**
- * End first or second animator if they are still running.
- */
- static void endAll(ValueAnimator first, ValueAnimator second) {
- if (first.isStarted()) {
- first.end();
- } else if (second.isStarted()) {
- second.end();
- }
- }
-
- /**
- * Fade in or fade out rows and background.
- *
- * @param show True to fade in, false to fade out.
- * @param animation True to run animation.
- */
- void showControlsOverlay(boolean show, boolean animation) {
- if (DEBUG) Log.v(TAG, "showControlsOverlay " + show);
- if (getView() == null) {
- mControlVisibleBeforeOnCreateView = show;
- return;
- }
- // force no animation when fragment is not resumed
- if (!isResumed()) {
- animation = false;
- }
- if (show == mControlVisible) {
- if (!animation) {
- // End animation if needed
- endAll(mBgFadeInAnimator, mBgFadeOutAnimator);
- endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);
- endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);
- }
- return;
- }
- // StateGraph: 7<->5 4<->3 2->3
- mControlVisible = show;
- if (!mControlVisible) {
- // StateGraph 2->3
- stopFadeTimer();
- }
-
- mAnimationTranslateY = (getVerticalGridView() == null
- || getVerticalGridView().getSelectedPosition() == 0)
- ? mMajorFadeTranslateY : mMinorFadeTranslateY;
-
- if (show) {
- reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);
- reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,
- animation);
- reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);
- } else {
- reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);
- reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,
- animation);
- reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);
- }
- if (animation) {
- getView().announceForAccessibility(getString(show
- ? R.string.lb_playback_controls_shown
- : R.string.lb_playback_controls_hidden));
- }
- }
-
- /**
- * Sets the selected row position with smooth animation.
- */
- public void setSelectedPosition(int position) {
- setSelectedPosition(position, true);
- }
-
- /**
- * Sets the selected row position.
- */
- public void setSelectedPosition(int position, boolean smooth) {
- mSetSelectionRunnable.mPosition = position;
- mSetSelectionRunnable.mSmooth = smooth;
- if (getView() != null && getView().getHandler() != null) {
- getView().getHandler().post(mSetSelectionRunnable);
- }
- }
-
- private void setupChildFragmentLayout() {
- setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());
- }
-
- void setVerticalGridViewLayout(VerticalGridView listview) {
- if (listview == null) {
- return;
- }
-
- // we set the base line of alignment to -paddingBottom
- listview.setWindowAlignmentOffset(-mPaddingBottom);
- listview.setWindowAlignmentOffsetPercent(
- VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
-
- // align other rows that arent the last to center of screen, since our baseline is
- // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom.
- listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom);
- listview.setItemAlignmentOffsetPercent(50);
-
- // Push last row to the bottom padding
- // Padding affects alignment when last row is focused
- listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(),
- listview.getPaddingRight(), mPaddingBottom);
- listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mOtherRowsCenterToBottom = getResources()
- .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom);
- mPaddingBottom =
- getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom);
- mBgDarkColor =
- getResources().getColor(R.color.lb_playback_controls_background_dark);
- mBgLightColor =
- getResources().getColor(R.color.lb_playback_controls_background_light);
- mShowTimeMs =
- getResources().getInteger(R.integer.lb_playback_controls_show_time_ms);
- mMajorFadeTranslateY =
- getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y);
- mMinorFadeTranslateY =
- getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y);
-
- loadBgAnimator();
- loadControlRowAnimator();
- loadOtherRowAnimator();
- }
-
- /**
- * Sets the background type.
- *
- * @param type One of BG_LIGHT, BG_DARK, or BG_NONE.
- */
- public void setBackgroundType(int type) {
- switch (type) {
- case BG_LIGHT:
- case BG_DARK:
- case BG_NONE:
- if (type != mBackgroundType) {
- mBackgroundType = type;
- updateBackground();
- }
- break;
- default:
- throw new IllegalArgumentException("Invalid background type");
- }
- }
-
- /**
- * Returns the background type.
- */
- public int getBackgroundType() {
- return mBackgroundType;
- }
-
- private void updateBackground() {
- if (mBackgroundView != null) {
- int color = mBgDarkColor;
- switch (mBackgroundType) {
- case BG_DARK:
- break;
- case BG_LIGHT:
- color = mBgLightColor;
- break;
- case BG_NONE:
- color = Color.TRANSPARENT;
- break;
- }
- mBackgroundView.setBackground(new ColorDrawable(color));
- setBgAlpha(mBgAlpha);
- }
- }
-
- private final ItemBridgeAdapter.AdapterListener mAdapterListener =
- new ItemBridgeAdapter.AdapterListener() {
- @Override
- public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
- if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
- if (!mControlVisible) {
- if (DEBUG) Log.v(TAG, "setting alpha to 0");
- vh.getViewHolder().view.setAlpha(0);
- }
- }
-
- @Override
- public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
- Presenter.ViewHolder viewHolder = vh.getViewHolder();
- if (viewHolder instanceof PlaybackSeekUi) {
- ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);
- }
- }
-
- @Override
- public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
- if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
- // Reset animation state
- vh.getViewHolder().view.setAlpha(1f);
- vh.getViewHolder().view.setTranslationY(0);
- vh.getViewHolder().view.setAlpha(1f);
- }
-
- @Override
- public void onBind(ItemBridgeAdapter.ViewHolder vh) {
- }
- };
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
- mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
- mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
- R.id.playback_controls_dock);
- if (mRowsFragment == null) {
- mRowsFragment = new RowsFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.playback_controls_dock, mRowsFragment)
- .commit();
- }
- if (mAdapter == null) {
- setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector()));
- } else {
- mRowsFragment.setAdapter(mAdapter);
- }
- mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
-
- mBgAlpha = 255;
- updateBackground();
- mRowsFragment.setExternalAdapterListener(mAdapterListener);
- ProgressBarManager progressBarManager = getProgressBarManager();
- if (progressBarManager != null) {
- progressBarManager.setRootView((ViewGroup) mRootView);
- }
- return mRootView;
- }
-
- /**
- * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will
- * take appropriate actions to take action when the hosting fragment starts/stops processing.
- */
- public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) {
- this.mHostCallback = hostCallback;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- setupChildFragmentLayout();
- mRowsFragment.setAdapter(mAdapter);
- if (mHostCallback != null) {
- mHostCallback.onHostStart();
- }
- }
-
- @Override
- public void onStop() {
- if (mHostCallback != null) {
- mHostCallback.onHostStop();
- }
- super.onStop();
- }
-
- @Override
- public void onPause() {
- if (mHostCallback != null) {
- mHostCallback.onHostPause();
- }
- if (mHandler.hasMessages(START_FADE_OUT)) {
- // StateGraph: 2->1
- mHandler.removeMessages(START_FADE_OUT);
- } else {
- // StateGraph: 5->6, 7->6, 4->1, 3->1
- }
- super.onPause();
- }
-
- /**
- * This listener is called every time there is a selection in {@link RowsFragment}. This can
- * be used by users to take additional actions such as animations.
- */
- public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
- mExternalItemSelectedListener = listener;
- }
-
- /**
- * This listener is called every time there is a click in {@link RowsFragment}. This can
- * be used by users to take additional actions such as animations.
- */
- public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
- mExternalItemClickedListener = listener;
- }
-
- /**
- * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks
- * only on {@link android.support.v17.leanback.widget.PlaybackRowPresenter.ViewHolder}.
- */
- public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
- mPlaybackItemClickedListener = listener;
- }
-
- @Override
- public void onDestroyView() {
- mRootView = null;
- mBackgroundView = null;
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- if (mHostCallback != null) {
- mHostCallback.onHostDestroy();
- }
- super.onDestroy();
- }
-
- /**
- * Sets the playback row for the playback controls. The row will be set as first element
- * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}.
- * @param row The row that represents the playback.
- */
- public void setPlaybackRow(Row row) {
- this.mRow = row;
- setupRow();
- setupPresenter();
- }
-
- /**
- * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If
- * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will
- * create a {@link ClassPresenterSelector} by default and map from the row object class to this
- * {@link PlaybackRowPresenter}.
- *
- * @param presenter Presenter used to render {@link #setPlaybackRow(Row)}.
- */
- public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
- this.mPresenter = presenter;
- setupPresenter();
- setPlaybackRowPresenterAlignment();
- }
-
- void setPlaybackRowPresenterAlignment() {
- if (mAdapter != null && mAdapter.getPresenterSelector() != null) {
- Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters();
- if (presenters != null) {
- for (int i = 0; i < presenters.length; i++) {
- if (presenters[i] instanceof PlaybackRowPresenter
- && presenters[i].getFacet(ItemAlignmentFacet.class) == null) {
- ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet();
- ItemAlignmentFacet.ItemAlignmentDef def =
- new ItemAlignmentFacet.ItemAlignmentDef();
- def.setItemAlignmentOffset(0);
- def.setItemAlignmentOffsetPercent(100);
- itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]
- {def});
- presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment);
- }
- }
- }
- }
- }
-
- /**
- * Updates the ui when the row data changes.
- */
- public void notifyPlaybackRowChanged() {
- if (mAdapter == null) {
- return;
- }
- mAdapter.notifyItemRangeChanged(0, 1);
- }
-
- /**
- * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be
- * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides
- * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)},
- * the row and presenter will be set onto the adapter.
- *
- * @param adapter The adapter that contains related rows and optional playback row.
- */
- public void setAdapter(ObjectAdapter adapter) {
- mAdapter = adapter;
- setupRow();
- setupPresenter();
- setPlaybackRowPresenterAlignment();
-
- if (mRowsFragment != null) {
- mRowsFragment.setAdapter(adapter);
- }
- }
-
- private void setupRow() {
- if (mAdapter instanceof ArrayObjectAdapter && mRow != null) {
- ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter);
- if (adapter.size() == 0) {
- adapter.add(mRow);
- } else {
- adapter.replace(0, mRow);
- }
- } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) {
- SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter);
- adapter.set(0, mRow);
- }
- }
-
- private void setupPresenter() {
- if (mAdapter != null && mRow != null && mPresenter != null) {
- PresenterSelector selector = mAdapter.getPresenterSelector();
- if (selector == null) {
- selector = new ClassPresenterSelector();
- ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
- mAdapter.setPresenterSelector(selector);
- } else if (selector instanceof ClassPresenterSelector) {
- ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
- }
- }
- }
-
- final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {
- @Override
- public boolean isSeekEnabled() {
- return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();
- }
-
- @Override
- public void onSeekStarted() {
- if (mSeekUiClient != null) {
- mSeekUiClient.onSeekStarted();
- }
- setSeekMode(true);
- }
-
- @Override
- public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
- return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();
- }
-
- @Override
- public void onSeekPositionChanged(long pos) {
- if (mSeekUiClient != null) {
- mSeekUiClient.onSeekPositionChanged(pos);
- }
- }
-
- @Override
- public void onSeekFinished(boolean cancelled) {
- if (mSeekUiClient != null) {
- mSeekUiClient.onSeekFinished(cancelled);
- }
- setSeekMode(false);
- }
- };
-
- /**
- * Interface to be implemented by UI widget to support PlaybackSeekUi.
- */
- public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {
- mSeekUiClient = client;
- }
-
- /**
- * Show or hide other rows other than PlaybackRow.
- * @param inSeek True to make other rows visible, false to make other rows invisible.
- */
- void setSeekMode(boolean inSeek) {
- if (mInSeek == inSeek) {
- return;
- }
- mInSeek = inSeek;
- getVerticalGridView().setSelectedPosition(0);
- if (mInSeek) {
- stopFadeTimer();
- }
- // immediately fade in control row.
- showControlsOverlay(true);
- final int count = getVerticalGridView().getChildCount();
- for (int i = 0; i < count; i++) {
- View view = getVerticalGridView().getChildAt(i);
- if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
- view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);
- }
- }
- }
-
- /**
- * Called when size of the video changes. App may override.
- * @param videoWidth Intrinsic width of video
- * @param videoHeight Intrinsic height of video
- */
- protected void onVideoSizeChanged(int videoWidth, int videoHeight) {
- }
-
- /**
- * Called when media has start or stop buffering. App may override. The default initial state
- * is not buffering.
- * @param start True for buffering start, false otherwise.
- */
- protected void onBufferingStateChanged(boolean start) {
- ProgressBarManager progressBarManager = getProgressBarManager();
- if (progressBarManager != null) {
- if (start) {
- progressBarManager.show();
- } else {
- progressBarManager.hide();
- }
- }
- }
-
- /**
- * Called when media has error. App may override.
- * @param errorCode Optional error code for specific implementation.
- * @param errorMessage Optional error message for specific implementation.
- */
- protected void onError(int errorCode, CharSequence errorMessage) {
- }
-
- /**
- * Returns the ProgressBarManager that will show or hide progress bar in
- * {@link #onBufferingStateChanged(boolean)}.
- * @return The ProgressBarManager that will show or hide progress bar in
- * {@link #onBufferingStateChanged(boolean)}.
- */
- public ProgressBarManager getProgressBarManager() {
- return mProgressBarManager;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
deleted file mode 100644
index 4a9d10f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.PlaybackRowPresenter;
-import android.support.v17.leanback.widget.PlaybackSeekUi;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.view.View;
-
-/**
- * {@link PlaybackGlueHost} implementation
- * the interaction between this class and {@link PlaybackFragment}.
- */
-public class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {
- private final PlaybackFragment mFragment;
-
- public PlaybackFragmentGlueHost(PlaybackFragment fragment) {
- this.mFragment = fragment;
- }
-
- @Override
- public void setControlsOverlayAutoHideEnabled(boolean enabled) {
- mFragment.setControlsOverlayAutoHideEnabled(enabled);
- }
-
- @Override
- public boolean isControlsOverlayAutoHideEnabled() {
- return mFragment.isControlsOverlayAutoHideEnabled();
- }
-
- @Override
- public void setOnKeyInterceptListener(View.OnKeyListener onKeyListener) {
- mFragment.setOnKeyInterceptListener(onKeyListener);
- }
-
- @Override
- public void setOnActionClickedListener(final OnActionClickedListener listener) {
- if (listener == null) {
- mFragment.setOnPlaybackItemViewClickedListener(null);
- } else {
- mFragment.setOnPlaybackItemViewClickedListener(new OnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- if (item instanceof Action) {
- listener.onActionClicked((Action) item);
- }
- }
- });
- }
- }
-
- @Override
- public void setHostCallback(HostCallback callback) {
- mFragment.setHostCallback(callback);
- }
-
- @Override
- public void notifyPlaybackRowChanged() {
- mFragment.notifyPlaybackRowChanged();
- }
-
- @Override
- public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
- mFragment.setPlaybackRowPresenter(presenter);
- }
-
- @Override
- public void setPlaybackRow(Row row) {
- mFragment.setPlaybackRow(row);
- }
-
- @Override
- public void fadeOut() {
- mFragment.fadeOut();
- }
-
- @Override
- public boolean isControlsOverlayVisible() {
- return mFragment.isControlsOverlayVisible();
- }
-
- @Override
- public void hideControlsOverlay(boolean runAnimation) {
- mFragment.hideControlsOverlay(runAnimation);
- }
-
- @Override
- public void showControlsOverlay(boolean runAnimation) {
- mFragment.showControlsOverlay(runAnimation);
- }
-
- @Override
- public void setPlaybackSeekUiClient(Client client) {
- mFragment.setPlaybackSeekUiClient(client);
- }
-
- final PlayerCallback mPlayerCallback =
- new PlayerCallback() {
- @Override
- public void onBufferingStateChanged(boolean start) {
- mFragment.onBufferingStateChanged(start);
- }
-
- @Override
- public void onError(int errorCode, CharSequence errorMessage) {
- mFragment.onError(errorCode, errorMessage);
- }
-
- @Override
- public void onVideoSizeChanged(int videoWidth, int videoHeight) {
- mFragment.onVideoSizeChanged(videoWidth, videoHeight);
- }
- };
-
- @Override
- public PlayerCallback getPlayerCallback() {
- return mPlayerCallback;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
deleted file mode 100644
index a008ad6..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ /dev/null
@@ -1,685 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from RowsSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.animation.TimeAnimator;
-import android.animation.TimeAnimator.TimeListener;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
-import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v17.leanback.widget.ViewHolderTask;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-
-import java.util.ArrayList;
-
-/**
- * An ordered set of rows of leanback widgets.
- * <p>
- * A RowsFragment renders the elements of its
- * {@link android.support.v17.leanback.widget.ObjectAdapter} as a set
- * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
- * of {@link RowPresenter}.
- * </p>
- */
-public class RowsFragment extends BaseRowFragment implements
- BrowseFragment.MainFragmentRowsAdapterProvider,
- BrowseFragment.MainFragmentAdapterProvider {
-
- private MainFragmentAdapter mMainFragmentAdapter;
- private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
-
- @Override
- public BrowseFragment.MainFragmentAdapter getMainFragmentAdapter() {
- if (mMainFragmentAdapter == null) {
- mMainFragmentAdapter = new MainFragmentAdapter(this);
- }
- return mMainFragmentAdapter;
- }
-
- @Override
- public BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter() {
- if (mMainFragmentRowsAdapter == null) {
- mMainFragmentRowsAdapter = new MainFragmentRowsAdapter(this);
- }
- return mMainFragmentRowsAdapter;
- }
-
- /**
- * Internal helper class that manages row select animation and apply a default
- * dim to each row.
- */
- final class RowViewHolderExtra implements TimeListener {
- final RowPresenter mRowPresenter;
- final Presenter.ViewHolder mRowViewHolder;
-
- final TimeAnimator mSelectAnimator = new TimeAnimator();
-
- int mSelectAnimatorDurationInUse;
- Interpolator mSelectAnimatorInterpolatorInUse;
- float mSelectLevelAnimStart;
- float mSelectLevelAnimDelta;
-
- RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) {
- mRowPresenter = (RowPresenter) ibvh.getPresenter();
- mRowViewHolder = ibvh.getViewHolder();
- mSelectAnimator.setTimeListener(this);
- }
-
- @Override
- public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
- if (mSelectAnimator.isRunning()) {
- updateSelect(totalTime, deltaTime);
- }
- }
-
- void updateSelect(long totalTime, long deltaTime) {
- float fraction;
- if (totalTime >= mSelectAnimatorDurationInUse) {
- fraction = 1;
- mSelectAnimator.end();
- } else {
- fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse);
- }
- if (mSelectAnimatorInterpolatorInUse != null) {
- fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction);
- }
- float level = mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta;
- mRowPresenter.setSelectLevel(mRowViewHolder, level);
- }
-
- void animateSelect(boolean select, boolean immediate) {
- mSelectAnimator.end();
- final float end = select ? 1 : 0;
- if (immediate) {
- mRowPresenter.setSelectLevel(mRowViewHolder, end);
- } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) {
- mSelectAnimatorDurationInUse = mSelectAnimatorDuration;
- mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator;
- mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder);
- mSelectLevelAnimDelta = end - mSelectLevelAnimStart;
- mSelectAnimator.start();
- }
- }
-
- }
-
- static final String TAG = "RowsFragment";
- static final boolean DEBUG = false;
- static final int ALIGN_TOP_NOT_SET = Integer.MIN_VALUE;
-
- ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
- private int mSubPosition;
- boolean mExpand = true;
- boolean mViewsCreated;
- private int mAlignedTop = ALIGN_TOP_NOT_SET;
- boolean mAfterEntranceTransition = true;
- boolean mFreezeRows;
-
- BaseOnItemViewSelectedListener mOnItemViewSelectedListener;
- BaseOnItemViewClickedListener mOnItemViewClickedListener;
-
- // Select animation and interpolator are not intended to be
- // exposed at this moment. They might be synced with vertical scroll
- // animation later.
- int mSelectAnimatorDuration;
- Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
-
- private RecyclerView.RecycledViewPool mRecycledViewPool;
- private ArrayList<Presenter> mPresenterMapper;
-
- ItemBridgeAdapter.AdapterListener mExternalAdapterListener;
-
- @Override
- protected VerticalGridView findGridViewFromRoot(View view) {
- return (VerticalGridView) view.findViewById(R.id.container_list);
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- * OnItemViewClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
- */
- public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) {
- mOnItemViewClickedListener = listener;
- if (mViewsCreated) {
- throw new IllegalStateException(
- "Item clicked listener must be set before views are created");
- }
- }
-
- /**
- * Returns the item clicked listener.
- */
- public BaseOnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- /**
- * @deprecated use {@link BrowseFragment#enableRowScaling(boolean)} instead.
- *
- * @param enable true to enable row scaling
- */
- @Deprecated
- public void enableRowScaling(boolean enable) {
- }
-
- /**
- * Set the visibility of titles/hovercard of browse rows.
- */
- public void setExpand(boolean expand) {
- mExpand = expand;
- VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- final int count = listView.getChildCount();
- if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count);
- for (int i = 0; i < count; i++) {
- View view = listView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder vh =
- (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
- setRowViewExpanded(vh, mExpand);
- }
- }
- }
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) {
- mOnItemViewSelectedListener = listener;
- VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- final int count = listView.getChildCount();
- for (int i = 0; i < count; i++) {
- View view = listView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
- listView.getChildViewHolder(view);
- getRowViewHolder(ibvh).setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- }
- }
- }
-
- /**
- * Returns an item selection listener.
- */
- public BaseOnItemViewSelectedListener getOnItemViewSelectedListener() {
- return mOnItemViewSelectedListener;
- }
-
- @Override
- void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,
- int position, int subposition) {
- if (mSelectedViewHolder != viewHolder || mSubPosition != subposition) {
- if (DEBUG) Log.v(TAG, "new row selected position " + position + " subposition "
- + subposition + " view " + viewHolder.itemView);
- mSubPosition = subposition;
- if (mSelectedViewHolder != null) {
- setRowViewSelected(mSelectedViewHolder, false, false);
- }
- mSelectedViewHolder = (ItemBridgeAdapter.ViewHolder) viewHolder;
- if (mSelectedViewHolder != null) {
- setRowViewSelected(mSelectedViewHolder, true, false);
- }
- }
- // When RowsFragment is embedded inside a page fragment, we want to show
- // the title view only when we're on the first row or there is no data.
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.getFragmentHost().showTitleView(position <= 0);
- }
- }
-
- /**
- * Get row ViewHolder at adapter position. Returns null if the row object is not in adapter or
- * the row object has not been bound to a row view.
- *
- * @param position Position of row in adapter.
- * @return Row ViewHolder at a given adapter position.
- */
- public RowPresenter.ViewHolder getRowViewHolder(int position) {
- VerticalGridView verticalView = getVerticalGridView();
- if (verticalView == null) {
- return null;
- }
- return getRowViewHolder((ItemBridgeAdapter.ViewHolder)
- verticalView.findViewHolderForAdapterPosition(position));
- }
-
- @Override
- int getLayoutResourceId() {
- return R.layout.lb_rows_fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mSelectAnimatorDuration = getResources().getInteger(
- R.integer.lb_browse_rows_anim_duration);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- if (DEBUG) Log.v(TAG, "onViewCreated");
- super.onViewCreated(view, savedInstanceState);
- // Align the top edge of child with id row_content.
- // Need set this for directly using RowsFragment.
- getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
- getVerticalGridView().setSaveChildrenPolicy(VerticalGridView.SAVE_LIMITED_CHILD);
-
- setAlignment(mAlignedTop);
-
- mRecycledViewPool = null;
- mPresenterMapper = null;
- if (mMainFragmentAdapter != null) {
- mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
- }
-
- }
-
- @Override
- public void onDestroyView() {
- mViewsCreated = false;
- super.onDestroyView();
- }
-
- void setExternalAdapterListener(ItemBridgeAdapter.AdapterListener listener) {
- mExternalAdapterListener = listener;
- }
-
- static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) {
- ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded);
- }
-
- static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected,
- boolean immediate) {
- RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
- extra.animateSelect(selected, immediate);
- ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
- }
-
- private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
- new ItemBridgeAdapter.AdapterListener() {
- @Override
- public void onAddPresenter(Presenter presenter, int type) {
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onAddPresenter(presenter, type);
- }
- }
-
- @Override
- public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
- VerticalGridView listView = getVerticalGridView();
- if (listView != null) {
- // set clip children false for slide animation
- listView.setClipChildren(false);
- }
- setupSharedViewPool(vh);
- mViewsCreated = true;
- vh.setExtraObject(new RowViewHolderExtra(vh));
- // selected state is initialized to false, then driven by grid view onChildSelected
- // events. When there is rebind, grid view fires onChildSelected event properly.
- // So we don't need do anything special later in onBind or onAttachedToWindow.
- setRowViewSelected(vh, false, true);
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onCreate(vh);
- }
- RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
- RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
- rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
-
- @Override
- public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
- if (DEBUG) Log.v(TAG, "onAttachToWindow");
- // All views share the same mExpand value. When we attach a view to grid view,
- // we should make sure it pick up the latest mExpand value we set early on other
- // attached views. For no-structure-change update, the view is rebound to new data,
- // but again it should use the unchanged mExpand value, so we don't need do any
- // thing in onBind.
- setRowViewExpanded(vh, mExpand);
- RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
- RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
- rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
-
- // freeze the rows attached after RowsFragment#freezeRows() is called
- rowPresenter.freeze(rowVh, mFreezeRows);
-
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onAttachedToWindow(vh);
- }
- }
-
- @Override
- public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
- if (mSelectedViewHolder == vh) {
- setRowViewSelected(mSelectedViewHolder, false, true);
- mSelectedViewHolder = null;
- }
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onDetachedFromWindow(vh);
- }
- }
-
- @Override
- public void onBind(ItemBridgeAdapter.ViewHolder vh) {
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onBind(vh);
- }
- }
-
- @Override
- public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
- setRowViewSelected(vh, false, true);
- if (mExternalAdapterListener != null) {
- mExternalAdapterListener.onUnbind(vh);
- }
- }
- };
-
- void setupSharedViewPool(ItemBridgeAdapter.ViewHolder bridgeVh) {
- RowPresenter rowPresenter = (RowPresenter) bridgeVh.getPresenter();
- RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(bridgeVh.getViewHolder());
-
- if (rowVh instanceof ListRowPresenter.ViewHolder) {
- HorizontalGridView view = ((ListRowPresenter.ViewHolder) rowVh).getGridView();
- // Recycled view pool is shared between all list rows
- if (mRecycledViewPool == null) {
- mRecycledViewPool = view.getRecycledViewPool();
- } else {
- view.setRecycledViewPool(mRecycledViewPool);
- }
-
- ItemBridgeAdapter bridgeAdapter =
- ((ListRowPresenter.ViewHolder) rowVh).getBridgeAdapter();
- if (mPresenterMapper == null) {
- mPresenterMapper = bridgeAdapter.getPresenterMapper();
- } else {
- bridgeAdapter.setPresenterMapper(mPresenterMapper);
- }
- }
- }
-
- @Override
- void updateAdapter() {
- super.updateAdapter();
- mSelectedViewHolder = null;
- mViewsCreated = false;
-
- ItemBridgeAdapter adapter = getBridgeAdapter();
- if (adapter != null) {
- adapter.setAdapterListener(mBridgeAdapterListener);
- }
- }
-
- @Override
- public boolean onTransitionPrepare() {
- boolean prepared = super.onTransitionPrepare();
- if (prepared) {
- freezeRows(true);
- }
- return prepared;
- }
-
- @Override
- public void onTransitionEnd() {
- super.onTransitionEnd();
- freezeRows(false);
- }
-
- private void freezeRows(boolean freeze) {
- mFreezeRows = freeze;
- VerticalGridView verticalView = getVerticalGridView();
- if (verticalView != null) {
- final int count = verticalView.getChildCount();
- for (int i = 0; i < count; i++) {
- ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
- verticalView.getChildViewHolder(verticalView.getChildAt(i));
- RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
- RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
- rowPresenter.freeze(vh, freeze);
- }
- }
- }
-
- /**
- * For rows that willing to participate entrance transition, this function
- * hide views if afterTransition is true, show views if afterTransition is false.
- */
- public void setEntranceTransitionState(boolean afterTransition) {
- mAfterEntranceTransition = afterTransition;
- VerticalGridView verticalView = getVerticalGridView();
- if (verticalView != null) {
- final int count = verticalView.getChildCount();
- for (int i = 0; i < count; i++) {
- ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
- verticalView.getChildViewHolder(verticalView.getChildAt(i));
- RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
- RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
- rowPresenter.setEntranceTransitionState(vh, mAfterEntranceTransition);
- }
- }
- }
-
- /**
- * Selects a Row and perform an optional task on the Row. For example
- * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
- * Scroll to 11th row and selects 6th item on that row. The method will be ignored if
- * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
- * ViewGroup, Bundle)}).
- *
- * @param rowPosition Which row to select.
- * @param smooth True to scroll to the row, false for no animation.
- * @param rowHolderTask Task to perform on the Row.
- */
- public void setSelectedPosition(int rowPosition, boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- VerticalGridView verticalView = getVerticalGridView();
- if (verticalView == null) {
- return;
- }
- ViewHolderTask task = null;
- if (rowHolderTask != null) {
- // This task will execute once the scroll completes. Once the scrolling finishes,
- // we will get a success callback to update selected row position. Since the
- // update to selected row position happens in a post, we want to ensure that this
- // gets called after that.
- task = new ViewHolderTask() {
- @Override
- public void run(final RecyclerView.ViewHolder rvh) {
- rvh.itemView.post(new Runnable() {
- @Override
- public void run() {
- rowHolderTask.run(
- getRowViewHolder((ItemBridgeAdapter.ViewHolder) rvh));
- }
- });
- }
- };
- }
-
- if (smooth) {
- verticalView.setSelectedPositionSmooth(rowPosition, task);
- } else {
- verticalView.setSelectedPosition(rowPosition, task);
- }
- }
-
- static RowPresenter.ViewHolder getRowViewHolder(ItemBridgeAdapter.ViewHolder ibvh) {
- if (ibvh == null) {
- return null;
- }
- RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
- return rowPresenter.getRowViewHolder(ibvh.getViewHolder());
- }
-
- public boolean isScrolling() {
- if (getVerticalGridView() == null) {
- return false;
- }
- return getVerticalGridView().getScrollState() != HorizontalGridView.SCROLL_STATE_IDLE;
- }
-
- @Override
- public void setAlignment(int windowAlignOffsetFromTop) {
- if (windowAlignOffsetFromTop == ALIGN_TOP_NOT_SET) {
- return;
- }
- mAlignedTop = windowAlignOffsetFromTop;
- final VerticalGridView gridView = getVerticalGridView();
-
- if (gridView != null) {
- gridView.setItemAlignmentOffset(0);
- gridView.setItemAlignmentOffsetPercent(
- VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- gridView.setItemAlignmentOffsetWithPadding(true);
- gridView.setWindowAlignmentOffset(mAlignedTop);
- // align to a fixed position from top
- gridView.setWindowAlignmentOffsetPercent(
- VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- }
- }
-
- /**
- * Find row ViewHolder by position in adapter.
- * @param position Position of row.
- * @return ViewHolder of Row.
- */
- public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
- if (mVerticalGridView == null) {
- return null;
- }
- return getRowViewHolder((ItemBridgeAdapter.ViewHolder) mVerticalGridView
- .findViewHolderForAdapterPosition(position));
- }
-
- public static class MainFragmentAdapter extends BrowseFragment.MainFragmentAdapter<RowsFragment> {
-
- public MainFragmentAdapter(RowsFragment fragment) {
- super(fragment);
- setScalingEnabled(true);
- }
-
- @Override
- public boolean isScrolling() {
- return getFragment().isScrolling();
- }
-
- @Override
- public void setExpand(boolean expand) {
- getFragment().setExpand(expand);
- }
-
- @Override
- public void setEntranceTransitionState(boolean state) {
- getFragment().setEntranceTransitionState(state);
- }
-
- @Override
- public void setAlignment(int windowAlignOffsetFromTop) {
- getFragment().setAlignment(windowAlignOffsetFromTop);
- }
-
- @Override
- public boolean onTransitionPrepare() {
- return getFragment().onTransitionPrepare();
- }
-
- @Override
- public void onTransitionStart() {
- getFragment().onTransitionStart();
- }
-
- @Override
- public void onTransitionEnd() {
- getFragment().onTransitionEnd();
- }
-
- }
-
- /**
- * The adapter that RowsFragment implements
- * BrowseFragment.MainFragmentRowsAdapter.
- * @see #getMainFragmentRowsAdapter().
- */
- public static class MainFragmentRowsAdapter
- extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> {
-
- public MainFragmentRowsAdapter(RowsFragment fragment) {
- super(fragment);
- }
-
- @Override
- public void setAdapter(ObjectAdapter adapter) {
- getFragment().setAdapter(adapter);
- }
-
- /**
- * Sets an item clicked listener on the fragment.
- */
- @Override
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- getFragment().setOnItemViewClickedListener(listener);
- }
-
- @Override
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- getFragment().setOnItemViewSelectedListener(listener);
- }
-
- @Override
- public void setSelectedPosition(int rowPosition,
- boolean smooth,
- final Presenter.ViewHolderTask rowHolderTask) {
- getFragment().setSelectedPosition(rowPosition, smooth, rowHolderTask);
- }
-
- @Override
- public void setSelectedPosition(int rowPosition, boolean smooth) {
- getFragment().setSelectedPosition(rowPosition, smooth);
- }
-
- @Override
- public int getSelectedPosition() {
- return getFragment().getSelectedPosition();
- }
-
- @Override
- public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
- return getFragment().findRowViewHolderByPosition(position);
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
deleted file mode 100644
index 2154ff2..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
+++ /dev/null
@@ -1,772 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from SearchSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
-import android.Manifest;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.speech.RecognizerIntent;
-import android.speech.SpeechRecognizer;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.ObjectAdapter.DataObserver;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter.ViewHolder;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SearchBar;
-import android.support.v17.leanback.widget.SearchOrbView;
-import android.support.v17.leanback.widget.SpeechRecognitionCallback;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.app.Fragment;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.inputmethod.CompletionInfo;
-import android.widget.FrameLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A fragment to handle searches. An application will supply an implementation
- * of the {@link SearchResultProvider} interface to handle the search and return
- * an {@link ObjectAdapter} containing the results. The results are rendered
- * into a {@link RowsFragment}, in the same way that they are in a {@link
- * BrowseFragment}.
- *
- * <p>A SpeechRecognizer object will be created for which your application will need to declare
- * android.permission.RECORD_AUDIO in AndroidManifest file. If app's target version is >= 23 and
- * the device version is >= 23, a permission dialog will show first time using speech recognition.
- * 0 will be used as requestCode in requestPermissions() call.
- * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)} is deprecated.
- * </p>
- * <p>
- * Speech recognition is automatically started when fragment is created, but
- * not when fragment is restored from an instance state. Activity may manually
- * call {@link #startRecognition()}, typically in onNewIntent().
- * </p>
- */
-public class SearchFragment extends Fragment {
- static final String TAG = SearchFragment.class.getSimpleName();
- static final boolean DEBUG = false;
-
- private static final String EXTRA_LEANBACK_BADGE_PRESENT = "LEANBACK_BADGE_PRESENT";
- private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
- private static final String ARG_QUERY = ARG_PREFIX + ".query";
- private static final String ARG_TITLE = ARG_PREFIX + ".title";
-
- static final long SPEECH_RECOGNITION_DELAY_MS = 300;
-
- static final int RESULTS_CHANGED = 0x1;
- static final int QUERY_COMPLETE = 0x2;
-
- static final int AUDIO_PERMISSION_REQUEST_CODE = 0;
-
- /**
- * Search API to be provided by the application.
- */
- public static interface SearchResultProvider {
- /**
- * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve
- * an ObjectAdapter that will contain the results to future updates of the search query.</p>
- *
- * <p>As results are retrieved, the application should use the data set notification methods
- * on the ObjectAdapter to instruct the SearchFragment to update the results.</p>
- *
- * @return ObjectAdapter The result object adapter.
- */
- public ObjectAdapter getResultsAdapter();
-
- /**
- * <p>Method invoked when the search query is updated.</p>
- *
- * <p>This is called as soon as the query changes; it is up to the application to add a
- * delay before actually executing the queries if needed.
- *
- * <p>This method might not always be called before onQueryTextSubmit gets called, in
- * particular for voice input.
- *
- * @param newQuery The current search query.
- * @return whether the results changed as a result of the new query.
- */
- public boolean onQueryTextChange(String newQuery);
-
- /**
- * Method invoked when the search query is submitted, either by dismissing the keyboard,
- * pressing search or next on the keyboard or when voice has detected the end of the query.
- *
- * @param query The query entered.
- * @return whether the results changed as a result of the query.
- */
- public boolean onQueryTextSubmit(String query);
- }
-
- final DataObserver mAdapterObserver = new DataObserver() {
- @Override
- public void onChanged() {
- // onChanged() may be called multiple times e.g. the provider add
- // rows to ArrayObjectAdapter one by one.
- mHandler.removeCallbacks(mResultsChangedCallback);
- mHandler.post(mResultsChangedCallback);
- }
- };
-
- final Handler mHandler = new Handler();
-
- final Runnable mResultsChangedCallback = new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.v(TAG, "results changed, new size " + mResultAdapter.size());
- if (mRowsFragment != null
- && mRowsFragment.getAdapter() != mResultAdapter) {
- if (!(mRowsFragment.getAdapter() == null && mResultAdapter.size() == 0)) {
- mRowsFragment.setAdapter(mResultAdapter);
- mRowsFragment.setSelectedPosition(0);
- }
- }
- updateSearchBarVisibility();
- mStatus |= RESULTS_CHANGED;
- if ((mStatus & QUERY_COMPLETE) != 0) {
- updateFocus();
- }
- updateSearchBarNextFocusId();
- }
- };
-
- /**
- * Runs when a new provider is set AND when the fragment view is created.
- */
- private final Runnable mSetSearchResultProvider = new Runnable() {
- @Override
- public void run() {
- if (mRowsFragment == null) {
- // We'll retry once we have a rows fragment
- return;
- }
- // Retrieve the result adapter
- ObjectAdapter adapter = mProvider.getResultsAdapter();
- if (DEBUG) Log.v(TAG, "Got results adapter " + adapter);
- if (adapter != mResultAdapter) {
- boolean firstTime = mResultAdapter == null;
- releaseAdapter();
- mResultAdapter = adapter;
- if (mResultAdapter != null) {
- mResultAdapter.registerObserver(mAdapterObserver);
- }
- if (DEBUG) {
- Log.v(TAG, "mResultAdapter " + mResultAdapter + " size "
- + (mResultAdapter == null ? 0 : mResultAdapter.size()));
- }
- // delay the first time to avoid setting a empty result adapter
- // until we got first onChange() from the provider
- if (!(firstTime && (mResultAdapter == null || mResultAdapter.size() == 0))) {
- mRowsFragment.setAdapter(mResultAdapter);
- }
- executePendingQuery();
- }
- updateSearchBarNextFocusId();
-
- if (DEBUG) {
- Log.v(TAG, "mAutoStartRecognition " + mAutoStartRecognition
- + " mResultAdapter " + mResultAdapter
- + " adapter " + mRowsFragment.getAdapter());
- }
- if (mAutoStartRecognition) {
- mHandler.removeCallbacks(mStartRecognitionRunnable);
- mHandler.postDelayed(mStartRecognitionRunnable, SPEECH_RECOGNITION_DELAY_MS);
- } else {
- updateFocus();
- }
- }
- };
-
- final Runnable mStartRecognitionRunnable = new Runnable() {
- @Override
- public void run() {
- mAutoStartRecognition = false;
- mSearchBar.startRecognition();
- }
- };
-
- RowsFragment mRowsFragment;
- SearchBar mSearchBar;
- SearchResultProvider mProvider;
- String mPendingQuery = null;
-
- OnItemViewSelectedListener mOnItemViewSelectedListener;
- private OnItemViewClickedListener mOnItemViewClickedListener;
- ObjectAdapter mResultAdapter;
- private SpeechRecognitionCallback mSpeechRecognitionCallback;
-
- private String mTitle;
- private Drawable mBadgeDrawable;
- private ExternalQuery mExternalQuery;
-
- private SpeechRecognizer mSpeechRecognizer;
-
- int mStatus;
- boolean mAutoStartRecognition = true;
-
- private boolean mIsPaused;
- private boolean mPendingStartRecognitionWhenPaused;
- private SearchBar.SearchBarPermissionListener mPermissionListener =
- new SearchBar.SearchBarPermissionListener() {
- @Override
- public void requestAudioPermission() {
- PermissionHelper.requestPermissions(SearchFragment.this,
- new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_PERMISSION_REQUEST_CODE);
- }
- };
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions,
- int[] grantResults) {
- if (requestCode == AUDIO_PERMISSION_REQUEST_CODE && permissions.length > 0) {
- if (permissions[0].equals(Manifest.permission.RECORD_AUDIO)
- && grantResults[0] == PERMISSION_GRANTED) {
- startRecognition();
- }
- }
- }
-
- /**
- * @param args Bundle to use for the arguments, if null a new Bundle will be created.
- */
- public static Bundle createArgs(Bundle args, String query) {
- return createArgs(args, query, null);
- }
-
- public static Bundle createArgs(Bundle args, String query, String title) {
- if (args == null) {
- args = new Bundle();
- }
- args.putString(ARG_QUERY, query);
- args.putString(ARG_TITLE, title);
- return args;
- }
-
- /**
- * Creates a search fragment with a given search query.
- *
- * <p>You should only use this if you need to start the search fragment with a
- * pre-filled query.
- *
- * @param query The search query to begin with.
- * @return A new SearchFragment.
- */
- public static SearchFragment newInstance(String query) {
- SearchFragment fragment = new SearchFragment();
- Bundle args = createArgs(null, query);
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (mAutoStartRecognition) {
- mAutoStartRecognition = savedInstanceState == null;
- }
- super.onCreate(savedInstanceState);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
-
- FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame);
- mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar);
- mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
- @Override
- public void onSearchQueryChange(String query) {
- if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s %s", query,
- null == mProvider ? "(null)" : mProvider));
- if (null != mProvider) {
- retrieveResults(query);
- } else {
- mPendingQuery = query;
- }
- }
-
- @Override
- public void onSearchQuerySubmit(String query) {
- if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query));
- submitQuery(query);
- }
-
- @Override
- public void onKeyboardDismiss(String query) {
- if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query));
- queryComplete();
- }
- });
- mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
- mSearchBar.setPermissionListener(mPermissionListener);
- applyExternalQuery();
-
- readArguments(getArguments());
- if (null != mBadgeDrawable) {
- setBadgeDrawable(mBadgeDrawable);
- }
- if (null != mTitle) {
- setTitle(mTitle);
- }
-
- // Inject the RowsFragment in the results container
- if (getChildFragmentManager().findFragmentById(R.id.lb_results_frame) == null) {
- mRowsFragment = new RowsFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.lb_results_frame, mRowsFragment).commit();
- } else {
- mRowsFragment = (RowsFragment) getChildFragmentManager()
- .findFragmentById(R.id.lb_results_frame);
- }
- mRowsFragment.setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- if (DEBUG) {
- int position = mRowsFragment.getSelectedPosition();
- Log.v(TAG, String.format("onItemSelected %d", position));
- }
- updateSearchBarVisibility();
- if (null != mOnItemViewSelectedListener) {
- mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
- rowViewHolder, row);
- }
- }
- });
- mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
- mRowsFragment.setExpand(true);
- if (null != mProvider) {
- onSetSearchResultProvider();
- }
- return root;
- }
-
- private void resultsAvailable() {
- if ((mStatus & QUERY_COMPLETE) != 0) {
- focusOnResults();
- }
- updateSearchBarNextFocusId();
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- VerticalGridView list = mRowsFragment.getVerticalGridView();
- int mContainerListAlignTop =
- getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top);
- list.setItemAlignmentOffset(0);
- list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- list.setWindowAlignmentOffset(mContainerListAlignTop);
- list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- // VerticalGridView should not be focusable (see b/26894680 for details).
- list.setFocusable(false);
- list.setFocusableInTouchMode(false);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mIsPaused = false;
- if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
- mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
- FragmentUtil.getContext(SearchFragment.this));
- mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
- }
- if (mPendingStartRecognitionWhenPaused) {
- mPendingStartRecognitionWhenPaused = false;
- mSearchBar.startRecognition();
- } else {
- // Ensure search bar state consistency when using external recognizer
- mSearchBar.stopRecognition();
- }
- }
-
- @Override
- public void onPause() {
- releaseRecognizer();
- mIsPaused = true;
- super.onPause();
- }
-
- @Override
- public void onDestroy() {
- releaseAdapter();
- super.onDestroy();
- }
-
- /**
- * Returns RowsFragment that shows result rows. RowsFragment is initialized after
- * SearchFragment.onCreateView().
- *
- * @return RowsFragment that shows result rows.
- */
- public RowsFragment getRowsFragment() {
- return mRowsFragment;
- }
-
- private void releaseRecognizer() {
- if (null != mSpeechRecognizer) {
- mSearchBar.setSpeechRecognizer(null);
- mSpeechRecognizer.destroy();
- mSpeechRecognizer = null;
- }
- }
-
- /**
- * Starts speech recognition. Typical use case is that
- * activity receives onNewIntent() call when user clicks a MIC button.
- * Note that SearchFragment automatically starts speech recognition
- * at first time created, there is no need to call startRecognition()
- * when fragment is created.
- */
- public void startRecognition() {
- if (mIsPaused) {
- mPendingStartRecognitionWhenPaused = true;
- } else {
- mSearchBar.startRecognition();
- }
- }
-
- /**
- * Sets the search provider that is responsible for returning results for the
- * search query.
- */
- public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
- if (mProvider != searchResultProvider) {
- mProvider = searchResultProvider;
- onSetSearchResultProvider();
- }
- }
-
- /**
- * Sets an item selection listener for the results.
- *
- * @param listener The item selection listener to be invoked when an item in
- * the search results is selected.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- mOnItemViewSelectedListener = listener;
- }
-
- /**
- * Sets an item clicked listener for the results.
- *
- * @param listener The item clicked listener to be invoked when an item in
- * the search results is clicked.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- if (listener != mOnItemViewClickedListener) {
- mOnItemViewClickedListener = listener;
- if (mRowsFragment != null) {
- mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
- }
- }
-
- /**
- * Sets the title string to be be shown in an empty search bar. The title
- * may be placed in a call-to-action, such as "Search <i>title</i>" or
- * "Speak to search <i>title</i>".
- */
- public void setTitle(String title) {
- mTitle = title;
- if (null != mSearchBar) {
- mSearchBar.setTitle(title);
- }
- }
-
- /**
- * Returns the title set in the search bar.
- */
- public String getTitle() {
- if (null != mSearchBar) {
- return mSearchBar.getTitle();
- }
- return null;
- }
-
- /**
- * Sets the badge drawable that will be shown inside the search bar next to
- * the title.
- */
- public void setBadgeDrawable(Drawable drawable) {
- mBadgeDrawable = drawable;
- if (null != mSearchBar) {
- mSearchBar.setBadgeDrawable(drawable);
- }
- }
-
- /**
- * Returns the badge drawable in the search bar.
- */
- public Drawable getBadgeDrawable() {
- if (null != mSearchBar) {
- return mSearchBar.getBadgeDrawable();
- }
- return null;
- }
-
- /**
- * Sets background color of not-listening state search orb.
- *
- * @param colors SearchOrbView.Colors.
- */
- public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
- if (mSearchBar != null) {
- mSearchBar.setSearchAffordanceColors(colors);
- }
- }
-
- /**
- * Sets background color of listening state search orb.
- *
- * @param colors SearchOrbView.Colors.
- */
- public void setSearchAffordanceColorsInListening(SearchOrbView.Colors colors) {
- if (mSearchBar != null) {
- mSearchBar.setSearchAffordanceColorsInListening(colors);
- }
- }
-
- /**
- * Displays the completions shown by the IME. An application may provide
- * a list of query completions that the system will show in the IME.
- *
- * @param completions A list of completions to show in the IME. Setting to
- * null or empty will clear the list.
- */
- public void displayCompletions(List<String> completions) {
- mSearchBar.displayCompletions(completions);
- }
-
- /**
- * Displays the completions shown by the IME. An application may provide
- * a list of query completions that the system will show in the IME.
- *
- * @param completions A list of completions to show in the IME. Setting to
- * null or empty will clear the list.
- */
- public void displayCompletions(CompletionInfo[] completions) {
- mSearchBar.displayCompletions(completions);
- }
-
- /**
- * Sets this callback to have the fragment pass speech recognition requests
- * to the activity rather than using a SpeechRecognizer object.
- * @deprecated Launching voice recognition activity is no longer supported. App should declare
- * android.permission.RECORD_AUDIO in AndroidManifest file.
- */
- @Deprecated
- public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) {
- mSpeechRecognitionCallback = callback;
- if (mSearchBar != null) {
- mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
- }
- if (callback != null) {
- releaseRecognizer();
- }
- }
-
- /**
- * Sets the text of the search query and optionally submits the query. Either
- * {@link SearchResultProvider#onQueryTextChange onQueryTextChange} or
- * {@link SearchResultProvider#onQueryTextSubmit onQueryTextSubmit} will be
- * called on the provider if it is set.
- *
- * @param query The search query to set.
- * @param submit Whether to submit the query.
- */
- public void setSearchQuery(String query, boolean submit) {
- if (DEBUG) Log.v(TAG, "setSearchQuery " + query + " submit " + submit);
- if (query == null) {
- return;
- }
- mExternalQuery = new ExternalQuery(query, submit);
- applyExternalQuery();
- if (mAutoStartRecognition) {
- mAutoStartRecognition = false;
- mHandler.removeCallbacks(mStartRecognitionRunnable);
- }
- }
-
- /**
- * Sets the text of the search query based on the {@link RecognizerIntent#EXTRA_RESULTS} in
- * the given intent, and optionally submit the query. If more than one result is present
- * in the results list, the first will be used.
- *
- * @param intent Intent received from a speech recognition service.
- * @param submit Whether to submit the query.
- */
- public void setSearchQuery(Intent intent, boolean submit) {
- ArrayList<String> matches = intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
- if (matches != null && matches.size() > 0) {
- setSearchQuery(matches.get(0), submit);
- }
- }
-
- /**
- * Returns an intent that can be used to request speech recognition.
- * Built from the base {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} plus
- * extras:
- *
- * <ul>
- * <li>{@link RecognizerIntent#EXTRA_LANGUAGE_MODEL} set to
- * {@link RecognizerIntent#LANGUAGE_MODEL_FREE_FORM}</li>
- * <li>{@link RecognizerIntent#EXTRA_PARTIAL_RESULTS} set to true</li>
- * <li>{@link RecognizerIntent#EXTRA_PROMPT} set to the search bar hint text</li>
- * </ul>
- *
- * For handling the intent returned from the service, see
- * {@link #setSearchQuery(Intent, boolean)}.
- */
- public Intent getRecognizerIntent() {
- Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
- recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
- RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
- recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
- if (mSearchBar != null && mSearchBar.getHint() != null) {
- recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, mSearchBar.getHint());
- }
- recognizerIntent.putExtra(EXTRA_LEANBACK_BADGE_PRESENT, mBadgeDrawable != null);
- return recognizerIntent;
- }
-
- void retrieveResults(String searchQuery) {
- if (DEBUG) Log.v(TAG, "retrieveResults " + searchQuery);
- if (mProvider.onQueryTextChange(searchQuery)) {
- mStatus &= ~QUERY_COMPLETE;
- }
- }
-
- void submitQuery(String query) {
- queryComplete();
- if (null != mProvider) {
- mProvider.onQueryTextSubmit(query);
- }
- }
-
- void queryComplete() {
- if (DEBUG) Log.v(TAG, "queryComplete");
- mStatus |= QUERY_COMPLETE;
- focusOnResults();
- }
-
- void updateSearchBarVisibility() {
- int position = mRowsFragment != null ? mRowsFragment.getSelectedPosition() : -1;
- mSearchBar.setVisibility(position <=0 || mResultAdapter == null
- || mResultAdapter.size() == 0 ? View.VISIBLE : View.GONE);
- }
-
- void updateSearchBarNextFocusId() {
- if (mSearchBar == null || mResultAdapter == null) {
- return;
- }
- final int viewId = (mResultAdapter.size() == 0 || mRowsFragment == null
- || mRowsFragment.getVerticalGridView() == null)
- ? 0 : mRowsFragment.getVerticalGridView().getId();
- mSearchBar.setNextFocusDownId(viewId);
- }
-
- void updateFocus() {
- if (mResultAdapter != null && mResultAdapter.size() > 0
- && mRowsFragment != null && mRowsFragment.getAdapter() == mResultAdapter) {
- focusOnResults();
- } else {
- mSearchBar.requestFocus();
- }
- }
-
- private void focusOnResults() {
- if (mRowsFragment == null || mRowsFragment.getVerticalGridView() == null
- || mResultAdapter.size() == 0) {
- return;
- }
- if (mRowsFragment.getVerticalGridView().requestFocus()) {
- mStatus &= ~RESULTS_CHANGED;
- }
- }
-
- private void onSetSearchResultProvider() {
- mHandler.removeCallbacks(mSetSearchResultProvider);
- mHandler.post(mSetSearchResultProvider);
- }
-
- void releaseAdapter() {
- if (mResultAdapter != null) {
- mResultAdapter.unregisterObserver(mAdapterObserver);
- mResultAdapter = null;
- }
- }
-
- void executePendingQuery() {
- if (null != mPendingQuery && null != mResultAdapter) {
- String query = mPendingQuery;
- mPendingQuery = null;
- retrieveResults(query);
- }
- }
-
- private void applyExternalQuery() {
- if (mExternalQuery == null || mSearchBar == null) {
- return;
- }
- mSearchBar.setSearchQuery(mExternalQuery.mQuery);
- if (mExternalQuery.mSubmit) {
- submitQuery(mExternalQuery.mQuery);
- }
- mExternalQuery = null;
- }
-
- private void readArguments(Bundle args) {
- if (null == args) {
- return;
- }
- if (args.containsKey(ARG_QUERY)) {
- setSearchQuery(args.getString(ARG_QUERY));
- }
-
- if (args.containsKey(ARG_TITLE)) {
- setTitle(args.getString(ARG_TITLE));
- }
- }
-
- private void setSearchQuery(String query) {
- mSearchBar.setSearchQuery(query);
- }
-
- static class ExternalQuery {
- String mQuery;
- boolean mSubmit;
-
- ExternalQuery(String query, boolean submit) {
- mQuery = query;
- mSubmit = submit;
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
deleted file mode 100644
index 5bc52ff..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ /dev/null
@@ -1,258 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VerticalGridSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.util.StateMachine.State;
-import android.support.v17.leanback.widget.BrowseFrameLayout;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnChildLaidOutListener;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.VerticalGridPresenter;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A fragment for creating leanback vertical grids.
- *
- * <p>Renders a vertical grid of objects given a {@link VerticalGridPresenter} and
- * an {@link ObjectAdapter}.
- */
-public class VerticalGridFragment extends BaseFragment {
- static final String TAG = "VerticalGF";
- static boolean DEBUG = false;
-
- private ObjectAdapter mAdapter;
- private VerticalGridPresenter mGridPresenter;
- VerticalGridPresenter.ViewHolder mGridViewHolder;
- OnItemViewSelectedListener mOnItemViewSelectedListener;
- private OnItemViewClickedListener mOnItemViewClickedListener;
- private Object mSceneAfterEntranceTransition;
- private int mSelectedPosition = -1;
-
- /**
- * State to setEntranceTransitionState(false)
- */
- final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
- @Override
- public void run() {
- setEntranceTransitionState(false);
- }
- };
-
- @Override
- void createStateMachineStates() {
- super.createStateMachineStates();
- mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
- }
-
- @Override
- void createStateMachineTransitions() {
- super.createStateMachineTransitions();
- mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
- STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
- }
-
- /**
- * Sets the grid presenter.
- */
- public void setGridPresenter(VerticalGridPresenter gridPresenter) {
- if (gridPresenter == null) {
- throw new IllegalArgumentException("Grid presenter may not be null");
- }
- mGridPresenter = gridPresenter;
- mGridPresenter.setOnItemViewSelectedListener(mViewSelectedListener);
- if (mOnItemViewClickedListener != null) {
- mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
- }
-
- /**
- * Returns the grid presenter.
- */
- public VerticalGridPresenter getGridPresenter() {
- return mGridPresenter;
- }
-
- /**
- * Sets the object adapter for the fragment.
- */
- public void setAdapter(ObjectAdapter adapter) {
- mAdapter = adapter;
- updateAdapter();
- }
-
- /**
- * Returns the object adapter.
- */
- public ObjectAdapter getAdapter() {
- return mAdapter;
- }
-
- final private OnItemViewSelectedListener mViewSelectedListener =
- new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- int position = mGridViewHolder.getGridView().getSelectedPosition();
- if (DEBUG) Log.v(TAG, "grid selected position " + position);
- gridOnItemSelected(position);
- if (mOnItemViewSelectedListener != null) {
- mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
- rowViewHolder, row);
- }
- }
- };
-
- final private OnChildLaidOutListener mChildLaidOutListener =
- new OnChildLaidOutListener() {
- @Override
- public void onChildLaidOut(ViewGroup parent, View view, int position, long id) {
- if (position == 0) {
- showOrHideTitle();
- }
- }
- };
-
- /**
- * Sets an item selection listener.
- */
- public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- mOnItemViewSelectedListener = listener;
- }
-
- void gridOnItemSelected(int position) {
- if (position != mSelectedPosition) {
- mSelectedPosition = position;
- showOrHideTitle();
- }
- }
-
- void showOrHideTitle() {
- if (mGridViewHolder.getGridView().findViewHolderForAdapterPosition(mSelectedPosition)
- == null) {
- return;
- }
- if (!mGridViewHolder.getGridView().hasPreviousViewInSameRow(mSelectedPosition)) {
- showTitle(true);
- } else {
- showTitle(false);
- }
- }
-
- /**
- * Sets an item clicked listener.
- */
- public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- mOnItemViewClickedListener = listener;
- if (mGridPresenter != null) {
- mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
- }
-
- /**
- * Returns the item clicked listener.
- */
- public OnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- ViewGroup root = (ViewGroup) inflater.inflate(R.layout.lb_vertical_grid_fragment,
- container, false);
- ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
- installTitleView(inflater, gridFrame, savedInstanceState);
- getProgressBarManager().setRootView(root);
-
- ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
- mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
- gridDock.addView(mGridViewHolder.view);
- mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
-
- mSceneAfterEntranceTransition = TransitionHelper.createScene(gridDock, new Runnable() {
- @Override
- public void run() {
- setEntranceTransitionState(true);
- }
- });
-
- updateAdapter();
- return root;
- }
-
- private void setupFocusSearchListener() {
- BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(
- R.id.grid_frame);
- browseFrameLayout.setOnFocusSearchListener(getTitleHelper().getOnFocusSearchListener());
- }
-
- @Override
- public void onStart() {
- super.onStart();
- setupFocusSearchListener();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mGridViewHolder = null;
- }
-
- /**
- * Sets the selected item position.
- */
- public void setSelectedPosition(int position) {
- mSelectedPosition = position;
- if(mGridViewHolder != null && mGridViewHolder.getGridView().getAdapter() != null) {
- mGridViewHolder.getGridView().setSelectedPositionSmooth(position);
- }
- }
-
- private void updateAdapter() {
- if (mGridViewHolder != null) {
- mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter);
- if (mSelectedPosition != -1) {
- mGridViewHolder.getGridView().setSelectedPosition(mSelectedPosition);
- }
- }
- }
-
- @Override
- protected Object createEntranceTransition() {
- return TransitionHelper.loadTransition(FragmentUtil.getContext(VerticalGridFragment.this),
- R.transition.lb_vertical_grid_entrance_transition);
- }
-
- @Override
- protected void runEntranceTransition(Object entranceTransition) {
- TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
- }
-
- void setEntranceTransitionState(boolean afterTransition) {
- mGridPresenter.setEntranceTransitionState(mGridViewHolder, afterTransition);
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
deleted file mode 100644
index 1b2b8d0..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VideoSupportFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.support.v17.leanback.R;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView}
- * and rendering video.
- */
-public class VideoFragment extends PlaybackFragment {
- static final int SURFACE_NOT_CREATED = 0;
- static final int SURFACE_CREATED = 1;
-
- SurfaceView mVideoSurface;
- SurfaceHolder.Callback mMediaPlaybackCallback;
-
- int mState = SURFACE_NOT_CREATED;
-
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
- mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(VideoFragment.this)).inflate(
- R.layout.lb_video_surface, root, false);
- root.addView(mVideoSurface, 0);
- mVideoSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
-
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- if (mMediaPlaybackCallback != null) {
- mMediaPlaybackCallback.surfaceCreated(holder);
- }
- mState = SURFACE_CREATED;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- if (mMediaPlaybackCallback != null) {
- mMediaPlaybackCallback.surfaceChanged(holder, format, width, height);
- }
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- if (mMediaPlaybackCallback != null) {
- mMediaPlaybackCallback.surfaceDestroyed(holder);
- }
- mState = SURFACE_NOT_CREATED;
- }
- });
- setBackgroundType(PlaybackFragment.BG_LIGHT);
- return root;
- }
-
- /**
- * Adds {@link SurfaceHolder.Callback} to {@link android.view.SurfaceView}.
- */
- public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
- mMediaPlaybackCallback = callback;
-
- if (callback != null) {
- if (mState == SURFACE_CREATED) {
- mMediaPlaybackCallback.surfaceCreated(mVideoSurface.getHolder());
- }
- }
- }
-
- @Override
- protected void onVideoSizeChanged(int width, int height) {
- int screenWidth = getView().getWidth();
- int screenHeight = getView().getHeight();
-
- ViewGroup.LayoutParams p = mVideoSurface.getLayoutParams();
- if (screenWidth * height > width * screenHeight) {
- // fit in screen height
- p.height = screenHeight;
- p.width = screenHeight * width / height;
- } else {
- // fit in screen width
- p.width = screenWidth;
- p.height = screenWidth * height / width;
- }
- mVideoSurface.setLayoutParams(p);
- }
-
- /**
- * Returns the surface view.
- */
- public SurfaceView getSurfaceView() {
- return mVideoSurface;
- }
-
- @Override
- public void onDestroyView() {
- mVideoSurface = null;
- mState = SURFACE_NOT_CREATED;
- super.onDestroyView();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
deleted file mode 100644
index d123676..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VideoSupportFragmentGlueHost.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.media.SurfaceHolderGlueHost;
-import android.view.SurfaceHolder;
-
-/**
- * {@link PlaybackGlueHost} implementation
- * the interaction between {@link PlaybackGlue} and {@link VideoFragment}.
- */
-public class VideoFragmentGlueHost extends PlaybackFragmentGlueHost
- implements SurfaceHolderGlueHost {
- private final VideoFragment mFragment;
-
- public VideoFragmentGlueHost(VideoFragment fragment) {
- super(fragment);
- this.mFragment = fragment;
- }
-
- /**
- * Sets the {@link android.view.SurfaceHolder.Callback} on the host.
- * {@link PlaybackGlueHost} is assumed to either host the {@link SurfaceHolder} or
- * have a reference to the component hosting it for rendering the video.
- */
- @Override
- public void setSurfaceHolderCallback(SurfaceHolder.Callback callback) {
- mFragment.setSurfaceHolderCallback(callback);
- }
-
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
deleted file mode 100644
index 00bc073..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import android.support.annotation.Nullable;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.util.ListUpdateCallback;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * An {@link ObjectAdapter} implemented with an {@link ArrayList}.
- */
-public class ArrayObjectAdapter extends ObjectAdapter {
-
- private static final Boolean DEBUG = false;
- private static final String TAG = "ArrayObjectAdapter";
-
- private final List mItems = new ArrayList<Object>();
-
- // To compute the payload correctly, we should use a temporary list to hold all the old items.
- private final List mOldItems = new ArrayList<Object>();
-
- // Un modifiable version of mItems;
- private List mUnmodifiableItems;
-
- /**
- * Constructs an adapter with the given {@link PresenterSelector}.
- */
- public ArrayObjectAdapter(PresenterSelector presenterSelector) {
- super(presenterSelector);
- }
-
- /**
- * Constructs an adapter that uses the given {@link Presenter} for all items.
- */
- public ArrayObjectAdapter(Presenter presenter) {
- super(presenter);
- }
-
- /**
- * Constructs an adapter.
- */
- public ArrayObjectAdapter() {
- super();
- }
-
- @Override
- public int size() {
- return mItems.size();
- }
-
- @Override
- public Object get(int index) {
- return mItems.get(index);
- }
-
- /**
- * Returns the index for the first occurrence of item in the adapter, or -1 if
- * not found.
- *
- * @param item The item to find in the list.
- * @return Index of the first occurrence of the item in the adapter, or -1
- * if not found.
- */
- public int indexOf(Object item) {
- return mItems.indexOf(item);
- }
-
- /**
- * Notify that the content of a range of items changed. Note that this is
- * not same as items being added or removed.
- *
- * @param positionStart The position of first item that has changed.
- * @param itemCount The count of how many items have changed.
- */
- public void notifyArrayItemRangeChanged(int positionStart, int itemCount) {
- notifyItemRangeChanged(positionStart, itemCount);
- }
-
- /**
- * Adds an item to the end of the adapter.
- *
- * @param item The item to add to the end of the adapter.
- */
- public void add(Object item) {
- add(mItems.size(), item);
- }
-
- /**
- * Inserts an item into this adapter at the specified index.
- * If the index is > {@link #size} an exception will be thrown.
- *
- * @param index The index at which the item should be inserted.
- * @param item The item to insert into the adapter.
- */
- public void add(int index, Object item) {
- mItems.add(index, item);
- notifyItemRangeInserted(index, 1);
- }
-
- /**
- * Adds the objects in the given collection to the adapter, starting at the
- * given index. If the index is >= {@link #size} an exception will be thrown.
- *
- * @param index The index at which the items should be inserted.
- * @param items A {@link Collection} of items to insert.
- */
- public void addAll(int index, Collection items) {
- int itemsCount = items.size();
- if (itemsCount == 0) {
- return;
- }
- mItems.addAll(index, items);
- notifyItemRangeInserted(index, itemsCount);
- }
-
- /**
- * Removes the first occurrence of the given item from the adapter.
- *
- * @param item The item to remove from the adapter.
- * @return True if the item was found and thus removed from the adapter.
- */
- public boolean remove(Object item) {
- int index = mItems.indexOf(item);
- if (index >= 0) {
- mItems.remove(index);
- notifyItemRangeRemoved(index, 1);
- }
- return index >= 0;
- }
-
- /**
- * Moved the item at fromPosition to toPosition.
- *
- * @param fromPosition Previous position of the item.
- * @param toPosition New position of the item.
- */
- public void move(int fromPosition, int toPosition) {
- if (fromPosition == toPosition) {
- // no-op
- return;
- }
- Object item = mItems.remove(fromPosition);
- mItems.add(toPosition, item);
- notifyItemMoved(fromPosition, toPosition);
- }
-
- /**
- * Replaces item at position with a new item and calls notifyItemRangeChanged()
- * at the given position. Note that this method does not compare new item to
- * existing item.
- *
- * @param position The index of item to replace.
- * @param item The new item to be placed at given position.
- */
- public void replace(int position, Object item) {
- mItems.set(position, item);
- notifyItemRangeChanged(position, 1);
- }
-
- /**
- * Removes a range of items from the adapter. The range is specified by giving
- * the starting position and the number of elements to remove.
- *
- * @param position The index of the first item to remove.
- * @param count The number of items to remove.
- * @return The number of items removed.
- */
- public int removeItems(int position, int count) {
- int itemsToRemove = Math.min(count, mItems.size() - position);
- if (itemsToRemove <= 0) {
- return 0;
- }
-
- for (int i = 0; i < itemsToRemove; i++) {
- mItems.remove(position);
- }
- notifyItemRangeRemoved(position, itemsToRemove);
- return itemsToRemove;
- }
-
- /**
- * Removes all items from this adapter, leaving it empty.
- */
- public void clear() {
- int itemCount = mItems.size();
- if (itemCount == 0) {
- return;
- }
- mItems.clear();
- notifyItemRangeRemoved(0, itemCount);
- }
-
- /**
- * Gets a read-only view of the list of object of this ArrayObjectAdapter.
- */
- public <E> List<E> unmodifiableList() {
-
- // The mUnmodifiableItems will only be created once as long as the content of mItems has not
- // been changed.
- if (mUnmodifiableItems == null) {
- mUnmodifiableItems = Collections.unmodifiableList(mItems);
- }
- return mUnmodifiableItems;
- }
-
- @Override
- public boolean isImmediateNotifySupported() {
- return true;
- }
-
- /**
- * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to
- * specified position.
- *
- * @param itemList List of new Items
- * @param callback Optional DiffCallback Object to compute the difference between the old data
- * set and new data set. When null, {@link #notifyChanged()} will be fired.
- */
- public void setItems(final List itemList, final DiffCallback callback) {
- if (callback == null) {
- // shortcut when DiffCallback is not provided
- mItems.clear();
- mItems.addAll(itemList);
- notifyChanged();
- return;
- }
- mOldItems.clear();
- mOldItems.addAll(mItems);
-
- DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
- @Override
- public int getOldListSize() {
- return mOldItems.size();
- }
-
- @Override
- public int getNewListSize() {
- return itemList.size();
- }
-
- @Override
- public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
- return callback.areItemsTheSame(mOldItems.get(oldItemPosition),
- itemList.get(newItemPosition));
- }
-
- @Override
- public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- return callback.areContentsTheSame(mOldItems.get(oldItemPosition),
- itemList.get(newItemPosition));
- }
-
- @Nullable
- @Override
- public Object getChangePayload(int oldItemPosition, int newItemPosition) {
- return callback.getChangePayload(mOldItems.get(oldItemPosition),
- itemList.get(newItemPosition));
- }
- });
-
- // update items.
- mItems.clear();
- mItems.addAll(itemList);
-
- // dispatch diff result
- diffResult.dispatchUpdatesTo(new ListUpdateCallback() {
-
- @Override
- public void onInserted(int position, int count) {
- if (DEBUG) {
- Log.d(TAG, "onInserted");
- }
- notifyItemRangeInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- if (DEBUG) {
- Log.d(TAG, "onRemoved");
- }
- notifyItemRangeRemoved(position, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- if (DEBUG) {
- Log.d(TAG, "onMoved");
- }
- notifyItemMoved(fromPosition, toPosition);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- if (DEBUG) {
- Log.d(TAG, "onChanged");
- }
- notifyItemRangeChanged(position, count, payload);
- }
- });
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
deleted file mode 100644
index f4e01c0..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ /dev/null
@@ -1,1202 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
-import android.support.annotation.RestrictTo;
-import android.support.v17.leanback.R;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.SimpleItemAnimator;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * An abstract base class for vertically and horizontally scrolling lists. The items come
- * from the {@link RecyclerView.Adapter} associated with this view.
- * Do not directly use this class, use {@link VerticalGridView} and {@link HorizontalGridView}.
- * The class is not intended to be subclassed other than {@link VerticalGridView} and
- * {@link HorizontalGridView}.
- */
-public abstract class BaseGridView extends RecyclerView {
-
- /**
- * Always keep focused item at a aligned position. Developer can use
- * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
- * In this mode, the last focused position will be remembered and restored when focus
- * is back to the view.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public final static int FOCUS_SCROLL_ALIGNED = 0;
-
- /**
- * Scroll to make the focused item inside client area.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public final static int FOCUS_SCROLL_ITEM = 1;
-
- /**
- * Scroll a page of items when focusing to item outside the client area.
- * The page size matches the client area size of RecyclerView.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public final static int FOCUS_SCROLL_PAGE = 2;
-
- /**
- * The first item is aligned with the low edge of the viewport. When
- * navigating away from the first item, the focus item is aligned to a key line location.
- * <p>
- * For HorizontalGridView, low edge refers to getPaddingLeft() when RTL is false or
- * getWidth() - getPaddingRight() when RTL is true.
- * For VerticalGridView, low edge refers to getPaddingTop().
- * <p>
- * The key line location is calculated by "windowAlignOffset" and
- * "windowAlignOffsetPercent"; if neither of these two is defined, the
- * default value is 1/2 of the size.
- * <p>
- * Note if there are very few items between low edge and key line, use
- * {@link #setWindowAlignmentPreferKeyLineOverLowEdge(boolean)} to control whether you prefer
- * to align the items to key line or low edge. Default is preferring low edge.
- */
- public final static int WINDOW_ALIGN_LOW_EDGE = 1;
-
- /**
- * The last item is aligned with the high edge of the viewport when
- * navigating to the end of list. When navigating away from the end, the
- * focus item is aligned to a key line location.
- * <p>
- * For HorizontalGridView, high edge refers to getWidth() - getPaddingRight() when RTL is false
- * or getPaddingLeft() when RTL is true.
- * For VerticalGridView, high edge refers to getHeight() - getPaddingBottom().
- * <p>
- * The key line location is calculated by "windowAlignOffset" and
- * "windowAlignOffsetPercent"; if neither of these two is defined, the
- * default value is 1/2 of the size.
- * <p>
- * Note if there are very few items between high edge and key line, use
- * {@link #setWindowAlignmentPreferKeyLineOverHighEdge(boolean)} to control whether you prefer
- * to align the items to key line or high edge. Default is preferring key line.
- */
- public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
-
- /**
- * The first item and last item are aligned with the two edges of the
- * viewport. When navigating in the middle of list, the focus maintains a
- * key line location.
- * <p>
- * The key line location is calculated by "windowAlignOffset" and
- * "windowAlignOffsetPercent"; if neither of these two is defined, the
- * default value is 1/2 of the size.
- */
- public final static int WINDOW_ALIGN_BOTH_EDGE =
- WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
-
- /**
- * The focused item always stays in a key line location.
- * <p>
- * The key line location is calculated by "windowAlignOffset" and
- * "windowAlignOffsetPercent"; if neither of these two is defined, the
- * default value is 1/2 of the size.
- */
- public final static int WINDOW_ALIGN_NO_EDGE = 0;
-
- /**
- * Value indicates that percent is not used.
- */
- public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1;
-
- /**
- * Value indicates that percent is not used.
- */
- public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED =
- ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED;
-
- /**
- * Dont save states of any child views.
- */
- public static final int SAVE_NO_CHILD = 0;
-
- /**
- * Only save on screen child views, the states are lost when they become off screen.
- */
- public static final int SAVE_ON_SCREEN_CHILD = 1;
-
- /**
- * Save on screen views plus save off screen child views states up to
- * {@link #getSaveChildrenLimitNumber()}.
- */
- public static final int SAVE_LIMITED_CHILD = 2;
-
- /**
- * Save on screen views plus save off screen child views without any limitation.
- * This might cause out of memory, only use it when you are dealing with limited data.
- */
- public static final int SAVE_ALL_CHILD = 3;
-
- /**
- * Listener for intercepting touch dispatch events.
- */
- public interface OnTouchInterceptListener {
- /**
- * Returns true if the touch dispatch event should be consumed.
- */
- public boolean onInterceptTouchEvent(MotionEvent event);
- }
-
- /**
- * Listener for intercepting generic motion dispatch events.
- */
- public interface OnMotionInterceptListener {
- /**
- * Returns true if the touch dispatch event should be consumed.
- */
- public boolean onInterceptMotionEvent(MotionEvent event);
- }
-
- /**
- * Listener for intercepting key dispatch events.
- */
- public interface OnKeyInterceptListener {
- /**
- * Returns true if the key dispatch event should be consumed.
- */
- public boolean onInterceptKeyEvent(KeyEvent event);
- }
-
- public interface OnUnhandledKeyListener {
- /**
- * Returns true if the key event should be consumed.
- */
- public boolean onUnhandledKey(KeyEvent event);
- }
-
- final GridLayoutManager mLayoutManager;
-
- /**
- * Animate layout changes from a child resizing or adding/removing a child.
- */
- private boolean mAnimateChildLayout = true;
-
- private boolean mHasOverlappingRendering = true;
-
- private RecyclerView.ItemAnimator mSavedItemAnimator;
-
- private OnTouchInterceptListener mOnTouchInterceptListener;
- private OnMotionInterceptListener mOnMotionInterceptListener;
- private OnKeyInterceptListener mOnKeyInterceptListener;
- RecyclerView.RecyclerListener mChainedRecyclerListener;
- private OnUnhandledKeyListener mOnUnhandledKeyListener;
-
- /**
- * Number of items to prefetch when first coming on screen with new data.
- */
- int mInitialPrefetchItemCount = 4;
-
- BaseGridView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mLayoutManager = new GridLayoutManager(this);
- setLayoutManager(mLayoutManager);
- // leanback LayoutManager already restores focus inside onLayoutChildren().
- setPreserveFocusAfterLayout(false);
- setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
- setHasFixedSize(true);
- setChildrenDrawingOrderEnabled(true);
- setWillNotDraw(true);
- setOverScrollMode(View.OVER_SCROLL_NEVER);
- // Disable change animation by default on leanback.
- // Change animation will create a new view and cause undesired
- // focus animation between the old view and new view.
- ((SimpleItemAnimator)getItemAnimator()).setSupportsChangeAnimations(false);
- super.setRecyclerListener(new RecyclerView.RecyclerListener() {
- @Override
- public void onViewRecycled(RecyclerView.ViewHolder holder) {
- mLayoutManager.onChildRecycled(holder);
- if (mChainedRecyclerListener != null) {
- mChainedRecyclerListener.onViewRecycled(holder);
- }
- }
- });
- }
-
- void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
- boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
- boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
- mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
- boolean throughSideStart = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideStart, true);
- boolean throughSideEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideEnd, true);
- mLayoutManager.setFocusOutSideAllowed(throughSideStart, throughSideEnd);
- mLayoutManager.setVerticalSpacing(
- a.getDimensionPixelSize(R.styleable.lbBaseGridView_android_verticalSpacing,
- a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0)));
- mLayoutManager.setHorizontalSpacing(
- a.getDimensionPixelSize(R.styleable.lbBaseGridView_android_horizontalSpacing,
- a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0)));
- if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
- setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
- }
- a.recycle();
- }
-
- /**
- * Sets the strategy used to scroll in response to item focus changing:
- * <ul>
- * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
- * <li>{@link #FOCUS_SCROLL_ITEM}</li>
- * <li>{@link #FOCUS_SCROLL_PAGE}</li>
- * </ul>
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setFocusScrollStrategy(int scrollStrategy) {
- if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
- && scrollStrategy != FOCUS_SCROLL_PAGE) {
- throw new IllegalArgumentException("Invalid scrollStrategy");
- }
- mLayoutManager.setFocusScrollStrategy(scrollStrategy);
- requestLayout();
- }
-
- /**
- * Returns the strategy used to scroll in response to item focus changing.
- * <ul>
- * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
- * <li>{@link #FOCUS_SCROLL_ITEM}</li>
- * <li>{@link #FOCUS_SCROLL_PAGE}</li>
- * </ul>
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public int getFocusScrollStrategy() {
- return mLayoutManager.getFocusScrollStrategy();
- }
-
- /**
- * Sets the method for focused item alignment in the view.
- *
- * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
- * {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
- * {@link #WINDOW_ALIGN_NO_EDGE}.
- */
- public void setWindowAlignment(int windowAlignment) {
- mLayoutManager.setWindowAlignment(windowAlignment);
- requestLayout();
- }
-
- /**
- * Returns the method for focused item alignment in the view.
- *
- * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
- * {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
- */
- public int getWindowAlignment() {
- return mLayoutManager.getWindowAlignment();
- }
-
- /**
- * Sets whether prefer key line over low edge when {@link #WINDOW_ALIGN_LOW_EDGE} is used.
- * When true, if there are very few items between low edge and key line, align items to key
- * line instead of align items to low edge.
- * Default value is false (aka prefer align to low edge).
- *
- * @param preferKeyLineOverLowEdge True to prefer key line over low edge, false otherwise.
- */
- public void setWindowAlignmentPreferKeyLineOverLowEdge(boolean preferKeyLineOverLowEdge) {
- mLayoutManager.mWindowAlignment.mainAxis()
- .setPreferKeylineOverLowEdge(preferKeyLineOverLowEdge);
- requestLayout();
- }
-
-
- /**
- * Returns whether prefer key line over high edge when {@link #WINDOW_ALIGN_HIGH_EDGE} is used.
- * When true, if there are very few items between high edge and key line, align items to key
- * line instead of align items to high edge.
- * Default value is true (aka prefer align to key line).
- *
- * @param preferKeyLineOverHighEdge True to prefer key line over high edge, false otherwise.
- */
- public void setWindowAlignmentPreferKeyLineOverHighEdge(boolean preferKeyLineOverHighEdge) {
- mLayoutManager.mWindowAlignment.mainAxis()
- .setPreferKeylineOverHighEdge(preferKeyLineOverHighEdge);
- requestLayout();
- }
-
- /**
- * Returns whether prefer key line over low edge when {@link #WINDOW_ALIGN_LOW_EDGE} is used.
- * When true, if there are very few items between low edge and key line, align items to key
- * line instead of align items to low edge.
- * Default value is false (aka prefer align to low edge).
- *
- * @return True to prefer key line over low edge, false otherwise.
- */
- public boolean isWindowAlignmentPreferKeyLineOverLowEdge() {
- return mLayoutManager.mWindowAlignment.mainAxis().isPreferKeylineOverLowEdge();
- }
-
-
- /**
- * Returns whether prefer key line over high edge when {@link #WINDOW_ALIGN_HIGH_EDGE} is used.
- * When true, if there are very few items between high edge and key line, align items to key
- * line instead of align items to high edge.
- * Default value is true (aka prefer align to key line).
- *
- * @return True to prefer key line over high edge, false otherwise.
- */
- public boolean isWindowAlignmentPreferKeyLineOverHighEdge() {
- return mLayoutManager.mWindowAlignment.mainAxis().isPreferKeylineOverHighEdge();
- }
-
-
- /**
- * Sets the offset in pixels for window alignment key line.
- *
- * @param offset The number of pixels to offset. If the offset is positive,
- * it is distance from low edge (see {@link #WINDOW_ALIGN_LOW_EDGE});
- * if the offset is negative, the absolute value is distance from high
- * edge (see {@link #WINDOW_ALIGN_HIGH_EDGE}).
- * Default value is 0.
- */
- public void setWindowAlignmentOffset(int offset) {
- mLayoutManager.setWindowAlignmentOffset(offset);
- requestLayout();
- }
-
- /**
- * Returns the offset in pixels for window alignment key line.
- *
- * @return The number of pixels to offset. If the offset is positive,
- * it is distance from low edge (see {@link #WINDOW_ALIGN_LOW_EDGE});
- * if the offset is negative, the absolute value is distance from high
- * edge (see {@link #WINDOW_ALIGN_HIGH_EDGE}).
- * Default value is 0.
- */
- public int getWindowAlignmentOffset() {
- return mLayoutManager.getWindowAlignmentOffset();
- }
-
- /**
- * Sets the offset percent for window alignment key line in addition to {@link
- * #getWindowAlignmentOffset()}.
- *
- * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
- * width from low edge. Use
- * {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
- * Default value is 50.
- */
- public void setWindowAlignmentOffsetPercent(float offsetPercent) {
- mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
- requestLayout();
- }
-
- /**
- * Returns the offset percent for window alignment key line in addition to
- * {@link #getWindowAlignmentOffset()}.
- *
- * @return Percentage to offset. E.g., 40 means 40% of the width from the
- * low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
- * disabled. Default value is 50.
- */
- public float getWindowAlignmentOffsetPercent() {
- return mLayoutManager.getWindowAlignmentOffsetPercent();
- }
-
- /**
- * Sets number of pixels to the end of low edge. Supports right to left layout direction.
- * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
- * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
- *
- * @param offset In left to right or vertical case, it's the offset added to left/top edge.
- * In right to left case, it's the offset subtracted from right edge.
- */
- public void setItemAlignmentOffset(int offset) {
- mLayoutManager.setItemAlignmentOffset(offset);
- requestLayout();
- }
-
- /**
- * Returns number of pixels to the end of low edge. Supports right to left layout direction. In
- * left to right or vertical case, it's the offset added to left/top edge. In right to left
- * case, it's the offset subtracted from right edge.
- * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
- * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
- *
- * @return The number of pixels to the end of low edge.
- */
- public int getItemAlignmentOffset() {
- return mLayoutManager.getItemAlignmentOffset();
- }
-
- /**
- * Sets whether applies padding to item alignment when {@link #getItemAlignmentOffsetPercent()}
- * is 0 or 100.
- * <p>When true:
- * Applies start/top padding if {@link #getItemAlignmentOffsetPercent()} is 0.
- * Applies end/bottom padding if {@link #getItemAlignmentOffsetPercent()} is 100.
- * Does not apply padding if {@link #getItemAlignmentOffsetPercent()} is neither 0 nor 100.
- * </p>
- * <p>When false: does not apply padding</p>
- */
- public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
- mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
- requestLayout();
- }
-
- /**
- * Returns true if applies padding to item alignment when
- * {@link #getItemAlignmentOffsetPercent()} is 0 or 100; returns false otherwise.
- * <p>When true:
- * Applies start/top padding when {@link #getItemAlignmentOffsetPercent()} is 0.
- * Applies end/bottom padding when {@link #getItemAlignmentOffsetPercent()} is 100.
- * Does not apply padding if {@link #getItemAlignmentOffsetPercent()} is neither 0 nor 100.
- * </p>
- * <p>When false: does not apply padding</p>
- */
- public boolean isItemAlignmentOffsetWithPadding() {
- return mLayoutManager.isItemAlignmentOffsetWithPadding();
- }
-
- /**
- * Sets the offset percent for item alignment in addition to {@link
- * #getItemAlignmentOffset()}.
- * Item alignment settings are ignored for the child if {@link ItemAlignmentFacet}
- * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
- *
- * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
- * width from the low edge. Use
- * {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
- */
- public void setItemAlignmentOffsetPercent(float offsetPercent) {
- mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
- requestLayout();
- }
-
- /**
- * Returns the offset percent for item alignment in addition to {@link
- * #getItemAlignmentOffset()}.
- *
- * @return Percentage to offset. E.g., 40 means 40% of the width from the
- * low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
- * disabled. Default value is 50.
- */
- public float getItemAlignmentOffsetPercent() {
- return mLayoutManager.getItemAlignmentOffsetPercent();
- }
-
- /**
- * Sets the id of the view to align with. Use {@link android.view.View#NO_ID} (default)
- * for the root {@link RecyclerView.ViewHolder#itemView}.
- * Item alignment settings on BaseGridView are if {@link ItemAlignmentFacet}
- * is provided by {@link RecyclerView.ViewHolder} or {@link FacetProviderAdapter}.
- */
- public void setItemAlignmentViewId(int viewId) {
- mLayoutManager.setItemAlignmentViewId(viewId);
- }
-
- /**
- * Returns the id of the view to align with, or {@link android.view.View#NO_ID} for the root
- * {@link RecyclerView.ViewHolder#itemView}.
- * @return The id of the view to align with, or {@link android.view.View#NO_ID} for the root
- * {@link RecyclerView.ViewHolder#itemView}.
- */
- public int getItemAlignmentViewId() {
- return mLayoutManager.getItemAlignmentViewId();
- }
-
- /**
- * Sets the spacing in pixels between two child items.
- * @deprecated use {@link #setItemSpacing(int)}
- */
- @Deprecated
- public void setItemMargin(int margin) {
- setItemSpacing(margin);
- }
-
- /**
- * Sets the vertical and horizontal spacing in pixels between two child items.
- * @param spacing Vertical and horizontal spacing in pixels between two child items.
- */
- public void setItemSpacing(int spacing) {
- mLayoutManager.setItemSpacing(spacing);
- requestLayout();
- }
-
- /**
- * Sets the spacing in pixels between two child items vertically.
- * @deprecated Use {@link #setVerticalSpacing(int)}
- */
- @Deprecated
- public void setVerticalMargin(int margin) {
- setVerticalSpacing(margin);
- }
-
- /**
- * Returns the spacing in pixels between two child items vertically.
- * @deprecated Use {@link #getVerticalSpacing()}
- */
- @Deprecated
- public int getVerticalMargin() {
- return mLayoutManager.getVerticalSpacing();
- }
-
- /**
- * Sets the spacing in pixels between two child items horizontally.
- * @deprecated Use {@link #setHorizontalSpacing(int)}
- */
- @Deprecated
- public void setHorizontalMargin(int margin) {
- setHorizontalSpacing(margin);
- }
-
- /**
- * Returns the spacing in pixels between two child items horizontally.
- * @deprecated Use {@link #getHorizontalSpacing()}
- */
- @Deprecated
- public int getHorizontalMargin() {
- return mLayoutManager.getHorizontalSpacing();
- }
-
- /**
- * Sets the vertical spacing in pixels between two child items.
- * @param spacing Vertical spacing between two child items.
- */
- public void setVerticalSpacing(int spacing) {
- mLayoutManager.setVerticalSpacing(spacing);
- requestLayout();
- }
-
- /**
- * Returns the vertical spacing in pixels between two child items.
- * @return The vertical spacing in pixels between two child items.
- */
- public int getVerticalSpacing() {
- return mLayoutManager.getVerticalSpacing();
- }
-
- /**
- * Sets the horizontal spacing in pixels between two child items.
- * @param spacing Horizontal spacing in pixels between two child items.
- */
- public void setHorizontalSpacing(int spacing) {
- mLayoutManager.setHorizontalSpacing(spacing);
- requestLayout();
- }
-
- /**
- * Returns the horizontal spacing in pixels between two child items.
- * @return The Horizontal spacing in pixels between two child items.
- */
- public int getHorizontalSpacing() {
- return mLayoutManager.getHorizontalSpacing();
- }
-
- /**
- * Registers a callback to be invoked when an item in BaseGridView has
- * been laid out.
- *
- * @param listener The listener to be invoked.
- */
- public void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
- mLayoutManager.setOnChildLaidOutListener(listener);
- }
-
- /**
- * Registers a callback to be invoked when an item in BaseGridView has
- * been selected. Note that the listener may be invoked when there is a
- * layout pending on the view, affording the listener an opportunity to
- * adjust the upcoming layout based on the selection state.
- *
- * @param listener The listener to be invoked.
- */
- public void setOnChildSelectedListener(OnChildSelectedListener listener) {
- mLayoutManager.setOnChildSelectedListener(listener);
- }
-
- /**
- * Registers a callback to be invoked when an item in BaseGridView has
- * been selected. Note that the listener may be invoked when there is a
- * layout pending on the view, affording the listener an opportunity to
- * adjust the upcoming layout based on the selection state.
- * This method will clear all existing listeners added by
- * {@link #addOnChildViewHolderSelectedListener}.
- *
- * @param listener The listener to be invoked.
- */
- public void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
- mLayoutManager.setOnChildViewHolderSelectedListener(listener);
- }
-
- /**
- * Registers a callback to be invoked when an item in BaseGridView has
- * been selected. Note that the listener may be invoked when there is a
- * layout pending on the view, affording the listener an opportunity to
- * adjust the upcoming layout based on the selection state.
- *
- * @param listener The listener to be invoked.
- */
- public void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
- mLayoutManager.addOnChildViewHolderSelectedListener(listener);
- }
-
- /**
- * Remove the callback invoked when an item in BaseGridView has been selected.
- *
- * @param listener The listener to be removed.
- */
- public void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener)
- {
- mLayoutManager.removeOnChildViewHolderSelectedListener(listener);
- }
-
- /**
- * Changes the selected item immediately without animation.
- */
- public void setSelectedPosition(int position) {
- mLayoutManager.setSelection(position, 0);
- }
-
- /**
- * Changes the selected item and/or subposition immediately without animation.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setSelectedPositionWithSub(int position, int subposition) {
- mLayoutManager.setSelectionWithSub(position, subposition, 0);
- }
-
- /**
- * Changes the selected item immediately without animation, scrollExtra is
- * applied in primary scroll direction. The scrollExtra will be kept until
- * another {@link #setSelectedPosition} or {@link #setSelectedPositionSmooth} call.
- */
- public void setSelectedPosition(int position, int scrollExtra) {
- mLayoutManager.setSelection(position, scrollExtra);
- }
-
- /**
- * Changes the selected item and/or subposition immediately without animation, scrollExtra is
- * applied in primary scroll direction. The scrollExtra will be kept until
- * another {@link #setSelectedPosition} or {@link #setSelectedPositionSmooth} call.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setSelectedPositionWithSub(int position, int subposition, int scrollExtra) {
- mLayoutManager.setSelectionWithSub(position, subposition, scrollExtra);
- }
-
- /**
- * Changes the selected item and run an animation to scroll to the target
- * position.
- * @param position Adapter position of the item to select.
- */
- public void setSelectedPositionSmooth(int position) {
- mLayoutManager.setSelectionSmooth(position);
- }
-
- /**
- * Changes the selected item and/or subposition, runs an animation to scroll to the target
- * position.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setSelectedPositionSmoothWithSub(int position, int subposition) {
- mLayoutManager.setSelectionSmoothWithSub(position, subposition);
- }
-
- /**
- * Perform a task on ViewHolder at given position after smooth scrolling to it.
- * @param position Position of item in adapter.
- * @param task Task to executed on the ViewHolder at a given position.
- */
- public void setSelectedPositionSmooth(final int position, final ViewHolderTask task) {
- if (task != null) {
- RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
- if (vh == null || hasPendingAdapterUpdates()) {
- addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
- @Override
- public void onChildViewHolderSelected(RecyclerView parent,
- RecyclerView.ViewHolder child, int selectedPosition, int subposition) {
- if (selectedPosition == position) {
- removeOnChildViewHolderSelectedListener(this);
- task.run(child);
- }
- }
- });
- } else {
- task.run(vh);
- }
- }
- setSelectedPositionSmooth(position);
- }
-
- /**
- * Perform a task on ViewHolder at given position after scroll to it.
- * @param position Position of item in adapter.
- * @param task Task to executed on the ViewHolder at a given position.
- */
- public void setSelectedPosition(final int position, final ViewHolderTask task) {
- if (task != null) {
- RecyclerView.ViewHolder vh = findViewHolderForPosition(position);
- if (vh == null || hasPendingAdapterUpdates()) {
- addOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
- @Override
- public void onChildViewHolderSelectedAndPositioned(RecyclerView parent,
- RecyclerView.ViewHolder child, int selectedPosition, int subposition) {
- if (selectedPosition == position) {
- removeOnChildViewHolderSelectedListener(this);
- task.run(child);
- }
- }
- });
- } else {
- task.run(vh);
- }
- }
- setSelectedPosition(position);
- }
-
- /**
- * Returns the adapter position of selected item.
- * @return The adapter position of selected item.
- */
- public int getSelectedPosition() {
- return mLayoutManager.getSelection();
- }
-
- /**
- * Returns the sub selected item position started from zero. An item can have
- * multiple {@link ItemAlignmentFacet}s provided by {@link RecyclerView.ViewHolder}
- * or {@link FacetProviderAdapter}. Zero is returned when no {@link ItemAlignmentFacet}
- * is defined.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public int getSelectedSubPosition() {
- return mLayoutManager.getSubSelection();
- }
-
- /**
- * Sets whether ItemAnimator should run when a child changes size or when adding
- * or removing a child.
- * @param animateChildLayout True to enable ItemAnimator, false to disable.
- */
- public void setAnimateChildLayout(boolean animateChildLayout) {
- if (mAnimateChildLayout != animateChildLayout) {
- mAnimateChildLayout = animateChildLayout;
- if (!mAnimateChildLayout) {
- mSavedItemAnimator = getItemAnimator();
- super.setItemAnimator(null);
- } else {
- super.setItemAnimator(mSavedItemAnimator);
- }
- }
- }
-
- /**
- * Returns true if an animation will run when a child changes size or when
- * adding or removing a child.
- * @return True if ItemAnimator is enabled, false otherwise.
- */
- public boolean isChildLayoutAnimated() {
- return mAnimateChildLayout;
- }
-
- /**
- * Sets the gravity used for child view positioning. Defaults to
- * GRAVITY_TOP|GRAVITY_START.
- *
- * @param gravity See {@link android.view.Gravity}
- */
- public void setGravity(int gravity) {
- mLayoutManager.setGravity(gravity);
- requestLayout();
- }
-
- @Override
- public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
- previouslyFocusedRect);
- }
-
- /**
- * Returns the x/y offsets to final position from current position if the view
- * is selected.
- *
- * @param view The view to get offsets.
- * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of Y.
- */
- public void getViewSelectedOffsets(View view, int[] offsets) {
- mLayoutManager.getViewSelectedOffsets(view, offsets);
- }
-
- @Override
- public int getChildDrawingOrder(int childCount, int i) {
- return mLayoutManager.getChildDrawingOrder(this, childCount, i);
- }
-
- final boolean isChildrenDrawingOrderEnabledInternal() {
- return isChildrenDrawingOrderEnabled();
- }
-
- @Override
- public View focusSearch(int direction) {
- if (isFocused()) {
- // focusSearch(int) is called when GridView itself is focused.
- // Calling focusSearch(view, int) to get next sibling of current selected child.
- View view = mLayoutManager.findViewByPosition(mLayoutManager.getSelection());
- if (view != null) {
- return focusSearch(view, direction);
- }
- }
- // otherwise, go to mParent to perform focusSearch
- return super.focusSearch(direction);
- }
-
- @Override
- protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- mLayoutManager.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- }
-
- /**
- * Disables or enables focus search.
- * @param disabled True to disable focus search, false to enable.
- */
- public final void setFocusSearchDisabled(boolean disabled) {
- // LayoutManager may detachView and attachView in fastRelayout, it causes RowsFragment
- // re-gain focus after a BACK key pressed, so block children focus during transition.
- setDescendantFocusability(disabled ? FOCUS_BLOCK_DESCENDANTS: FOCUS_AFTER_DESCENDANTS);
- mLayoutManager.setFocusSearchDisabled(disabled);
- }
-
- /**
- * Returns true if focus search is disabled.
- * @return True if focus search is disabled.
- */
- public final boolean isFocusSearchDisabled() {
- return mLayoutManager.isFocusSearchDisabled();
- }
-
- /**
- * Enables or disables layout. All children will be removed when layout is
- * disabled.
- * @param layoutEnabled True to enable layout, false otherwise.
- */
- public void setLayoutEnabled(boolean layoutEnabled) {
- mLayoutManager.setLayoutEnabled(layoutEnabled);
- }
-
- /**
- * Changes and overrides children's visibility.
- * @param visibility See {@link View#getVisibility()}.
- */
- public void setChildrenVisibility(int visibility) {
- mLayoutManager.setChildrenVisibility(visibility);
- }
-
- /**
- * Enables or disables pruning of children. Disable is useful during transition.
- * @param pruneChild True to prune children out side visible area, false to enable.
- */
- public void setPruneChild(boolean pruneChild) {
- mLayoutManager.setPruneChild(pruneChild);
- }
-
- /**
- * Enables or disables scrolling. Disable is useful during transition.
- * @param scrollEnabled True to enable scroll, false to disable.
- */
- public void setScrollEnabled(boolean scrollEnabled) {
- mLayoutManager.setScrollEnabled(scrollEnabled);
- }
-
- /**
- * Returns true if scrolling is enabled, false otherwise.
- * @return True if scrolling is enabled, false otherwise.
- */
- public boolean isScrollEnabled() {
- return mLayoutManager.isScrollEnabled();
- }
-
- /**
- * Returns true if the view at the given position has a same row sibling
- * in front of it. This will return true if first item view is not created.
- *
- * @param position Position in adapter.
- * @return True if the view at the given position has a same row sibling in front of it.
- */
- public boolean hasPreviousViewInSameRow(int position) {
- return mLayoutManager.hasPreviousViewInSameRow(position);
- }
-
- /**
- * Enables or disables the default "focus draw at last" order rule. Default is enabled.
- * @param enabled True to draw the selected child at last, false otherwise.
- */
- public void setFocusDrawingOrderEnabled(boolean enabled) {
- super.setChildrenDrawingOrderEnabled(enabled);
- }
-
- /**
- * Returns true if draws selected child at last, false otherwise. Default is enabled.
- * @return True if draws selected child at last, false otherwise.
- */
- public boolean isFocusDrawingOrderEnabled() {
- return super.isChildrenDrawingOrderEnabled();
- }
-
- /**
- * Sets the touch intercept listener.
- * @param listener The touch intercept listener.
- */
- public void setOnTouchInterceptListener(OnTouchInterceptListener listener) {
- mOnTouchInterceptListener = listener;
- }
-
- /**
- * Sets the generic motion intercept listener.
- * @param listener The motion intercept listener.
- */
- public void setOnMotionInterceptListener(OnMotionInterceptListener listener) {
- mOnMotionInterceptListener = listener;
- }
-
- /**
- * Sets the key intercept listener.
- * @param listener The key intercept listener.
- */
- public void setOnKeyInterceptListener(OnKeyInterceptListener listener) {
- mOnKeyInterceptListener = listener;
- }
-
- /**
- * Sets the unhandled key listener.
- * @param listener The unhandled key intercept listener.
- */
- public void setOnUnhandledKeyListener(OnUnhandledKeyListener listener) {
- mOnUnhandledKeyListener = listener;
- }
-
- /**
- * Returns the unhandled key listener.
- * @return The unhandled key listener.
- */
- public OnUnhandledKeyListener getOnUnhandledKeyListener() {
- return mOnUnhandledKeyListener;
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (mOnKeyInterceptListener != null && mOnKeyInterceptListener.onInterceptKeyEvent(event)) {
- return true;
- }
- if (super.dispatchKeyEvent(event)) {
- return true;
- }
- return mOnUnhandledKeyListener != null && mOnUnhandledKeyListener.onUnhandledKey(event);
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mOnTouchInterceptListener != null) {
- if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) {
- return true;
- }
- }
- return super.dispatchTouchEvent(event);
- }
-
- @Override
- protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
- if (mOnMotionInterceptListener != null) {
- if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) {
- return true;
- }
- }
- return super.dispatchGenericFocusedEvent(event);
- }
-
- /**
- * Returns the policy for saving children.
- *
- * @return policy, one of {@link #SAVE_NO_CHILD}
- * {@link #SAVE_ON_SCREEN_CHILD} {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
- */
- public final int getSaveChildrenPolicy() {
- return mLayoutManager.mChildrenStates.getSavePolicy();
- }
-
- /**
- * Returns the limit used when when {@link #getSaveChildrenPolicy()} is
- * {@link #SAVE_LIMITED_CHILD}
- */
- public final int getSaveChildrenLimitNumber() {
- return mLayoutManager.mChildrenStates.getLimitNumber();
- }
-
- /**
- * Sets the policy for saving children.
- * @param savePolicy One of {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
- * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
- */
- public final void setSaveChildrenPolicy(int savePolicy) {
- mLayoutManager.mChildrenStates.setSavePolicy(savePolicy);
- }
-
- /**
- * Sets the limit number when {@link #getSaveChildrenPolicy()} is {@link #SAVE_LIMITED_CHILD}.
- */
- public final void setSaveChildrenLimitNumber(int limitNumber) {
- mLayoutManager.mChildrenStates.setLimitNumber(limitNumber);
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return mHasOverlappingRendering;
- }
-
- public void setHasOverlappingRendering(boolean hasOverlapping) {
- mHasOverlappingRendering = hasOverlapping;
- }
-
- /**
- * Notify layout manager that layout directionality has been updated
- */
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- mLayoutManager.onRtlPropertiesChanged(layoutDirection);
- }
-
- @Override
- public void setRecyclerListener(RecyclerView.RecyclerListener listener) {
- mChainedRecyclerListener = listener;
- }
-
- /**
- * Sets pixels of extra space for layout child in invisible area.
- *
- * @param extraLayoutSpace Pixels of extra space for layout invisible child.
- * Must be bigger or equals to 0.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setExtraLayoutSpace(int extraLayoutSpace) {
- mLayoutManager.setExtraLayoutSpace(extraLayoutSpace);
- }
-
- /**
- * Returns pixels of extra space for layout child in invisible area.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public int getExtraLayoutSpace() {
- return mLayoutManager.getExtraLayoutSpace();
- }
-
- /**
- * Temporarily slide out child views to bottom (for VerticalGridView) or end
- * (for HorizontalGridView). Layout and scrolling will be suppressed until
- * {@link #animateIn()} is called.
- */
- public void animateOut() {
- mLayoutManager.slideOut();
- }
-
- /**
- * Undo animateOut() and slide in child views.
- */
- public void animateIn() {
- mLayoutManager.slideIn();
- }
-
- @Override
- public void scrollToPosition(int position) {
- // dont abort the animateOut() animation, just record the position
- if (mLayoutManager.mIsSlidingChildViews) {
- mLayoutManager.setSelectionWithSub(position, 0, 0);
- return;
- }
- super.scrollToPosition(position);
- }
-
- @Override
- public void smoothScrollToPosition(int position) {
- // dont abort the animateOut() animation, just record the position
- if (mLayoutManager.mIsSlidingChildViews) {
- mLayoutManager.setSelectionWithSub(position, 0, 0);
- return;
- }
- super.smoothScrollToPosition(position);
- }
-
- /**
- * Sets the number of items to prefetch in
- * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
- * which defines how many inner items should be prefetched when this GridView is nested inside
- * another RecyclerView.
- *
- * <p>Set this value to the number of items this inner GridView will display when it is
- * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items
- * so they are ready, avoiding jank as the inner GridView is scrolled into the viewport.</p>
- *
- * <p>For example, take a VerticalGridView of scrolling HorizontalGridViews. The rows always
- * have 6 items visible in them (or 7 if not aligned). Passing <code>6</code> to this method
- * for each inner GridView will enable RecyclerView's prefetching feature to do create/bind work
- * for 6 views within a row early, before it is scrolled on screen, instead of just the default
- * 4.</p>
- *
- * <p>Calling this method does nothing unless the LayoutManager is in a RecyclerView
- * nested in another RecyclerView.</p>
- *
- * <p class="note"><strong>Note:</strong> Setting this value to be larger than the number of
- * views that will be visible in this view can incur unnecessary bind work, and an increase to
- * the number of Views created and in active use.</p>
- *
- * @param itemCount Number of items to prefetch
- *
- * @see #getInitialPrefetchItemCount()
- * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
- * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
- */
- public void setInitialPrefetchItemCount(int itemCount) {
- mInitialPrefetchItemCount = itemCount;
- }
-
- /**
- * Gets the number of items to prefetch in
- * {@link RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)},
- * which defines how many inner items should be prefetched when this GridView is nested inside
- * another RecyclerView.
- *
- * @see RecyclerView.LayoutManager#isItemPrefetchEnabled()
- * @see #setInitialPrefetchItemCount(int)
- * @see RecyclerView.LayoutManager#collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
- *
- * @return number of items to prefetch.
- */
- public int getInitialPrefetchItemCount() {
- return mInitialPrefetchItemCount;
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
deleted file mode 100644
index af37f77..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ /dev/null
@@ -1,3712 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import static android.support.v7.widget.RecyclerView.HORIZONTAL;
-import static android.support.v7.widget.RecyclerView.NO_ID;
-import static android.support.v7.widget.RecyclerView.NO_POSITION;
-import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
-import static android.support.v7.widget.RecyclerView.VERTICAL;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.os.TraceCompat;
-import android.support.v4.util.CircularIntArray;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.widget.LinearSmoothScroller;
-import android.support.v7.widget.OrientationHelper;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.Recycler;
-import android.support.v7.widget.RecyclerView.State;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseIntArray;
-import android.view.FocusFinder;
-import android.view.Gravity;
-import android.view.View;
-import android.view.View.MeasureSpec;
-import android.view.ViewGroup;
-import android.view.ViewGroup.MarginLayoutParams;
-import android.view.animation.AccelerateDecelerateInterpolator;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-final class GridLayoutManager extends RecyclerView.LayoutManager {
-
- /*
- * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
- * The class currently does two internal jobs:
- * - Saves optical bounds insets.
- * - Caches focus align view center.
- */
- final static class LayoutParams extends RecyclerView.LayoutParams {
-
- // For placement
- int mLeftInset;
- int mTopInset;
- int mRightInset;
- int mBottomInset;
-
- // For alignment
- private int mAlignX;
- private int mAlignY;
- private int[] mAlignMultiple;
- private ItemAlignmentFacet mAlignmentFacet;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- public LayoutParams(ViewGroup.LayoutParams source) {
- super(source);
- }
-
- public LayoutParams(RecyclerView.LayoutParams source) {
- super(source);
- }
-
- public LayoutParams(LayoutParams source) {
- super(source);
- }
-
- int getAlignX() {
- return mAlignX;
- }
-
- int getAlignY() {
- return mAlignY;
- }
-
- int getOpticalLeft(View view) {
- return view.getLeft() + mLeftInset;
- }
-
- int getOpticalTop(View view) {
- return view.getTop() + mTopInset;
- }
-
- int getOpticalRight(View view) {
- return view.getRight() - mRightInset;
- }
-
- int getOpticalBottom(View view) {
- return view.getBottom() - mBottomInset;
- }
-
- int getOpticalWidth(View view) {
- return view.getWidth() - mLeftInset - mRightInset;
- }
-
- int getOpticalHeight(View view) {
- return view.getHeight() - mTopInset - mBottomInset;
- }
-
- int getOpticalLeftInset() {
- return mLeftInset;
- }
-
- int getOpticalRightInset() {
- return mRightInset;
- }
-
- int getOpticalTopInset() {
- return mTopInset;
- }
-
- int getOpticalBottomInset() {
- return mBottomInset;
- }
-
- void setAlignX(int alignX) {
- mAlignX = alignX;
- }
-
- void setAlignY(int alignY) {
- mAlignY = alignY;
- }
-
- void setItemAlignmentFacet(ItemAlignmentFacet facet) {
- mAlignmentFacet = facet;
- }
-
- ItemAlignmentFacet getItemAlignmentFacet() {
- return mAlignmentFacet;
- }
-
- void calculateItemAlignments(int orientation, View view) {
- ItemAlignmentFacet.ItemAlignmentDef[] defs = mAlignmentFacet.getAlignmentDefs();
- if (mAlignMultiple == null || mAlignMultiple.length != defs.length) {
- mAlignMultiple = new int[defs.length];
- }
- for (int i = 0; i < defs.length; i++) {
- mAlignMultiple[i] = ItemAlignmentFacetHelper
- .getAlignmentPosition(view, defs[i], orientation);
- }
- if (orientation == HORIZONTAL) {
- mAlignX = mAlignMultiple[0];
- } else {
- mAlignY = mAlignMultiple[0];
- }
- }
-
- int[] getAlignMultiple() {
- return mAlignMultiple;
- }
-
- void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
- mLeftInset = leftInset;
- mTopInset = topInset;
- mRightInset = rightInset;
- mBottomInset = bottomInset;
- }
-
- }
-
- /**
- * Base class which scrolls to selected view in onStop().
- */
- abstract class GridLinearSmoothScroller extends LinearSmoothScroller {
- GridLinearSmoothScroller() {
- super(mBaseGridView.getContext());
- }
-
- @Override
- protected void onStop() {
- // onTargetFound() may not be called if we hit the "wall" first or get cancelled.
- View targetView = findViewByPosition(getTargetPosition());
- if (targetView == null) {
- if (getTargetPosition() >= 0) {
- // if smooth scroller is stopped without target, immediately jumps
- // to the target position.
- scrollToSelection(getTargetPosition(), 0, false, 0);
- }
- super.onStop();
- return;
- }
- if (mFocusPosition != getTargetPosition()) {
- // This should not happen since we cropped value in startPositionSmoothScroller()
- mFocusPosition = getTargetPosition();
- }
- if (hasFocus()) {
- mInSelection = true;
- targetView.requestFocus();
- mInSelection = false;
- }
- dispatchChildSelected();
- dispatchChildSelectedAndPositioned();
- super.onStop();
- }
-
- @Override
- protected int calculateTimeForScrolling(int dx) {
- int ms = super.calculateTimeForScrolling(dx);
- if (mWindowAlignment.mainAxis().getSize() > 0) {
- float minMs = (float) MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN
- / mWindowAlignment.mainAxis().getSize() * dx;
- if (ms < minMs) {
- ms = (int) minMs;
- }
- }
- return ms;
- }
-
- @Override
- protected void onTargetFound(View targetView,
- RecyclerView.State state, Action action) {
- if (getScrollPosition(targetView, null, sTwoInts)) {
- int dx, dy;
- if (mOrientation == HORIZONTAL) {
- dx = sTwoInts[0];
- dy = sTwoInts[1];
- } else {
- dx = sTwoInts[1];
- dy = sTwoInts[0];
- }
- final int distance = (int) Math.sqrt(dx * dx + dy * dy);
- final int time = calculateTimeForDeceleration(distance);
- action.update(dx, dy, time, mDecelerateInterpolator);
- }
- }
- }
-
- /**
- * The SmoothScroller that remembers pending DPAD keys and consume pending keys
- * during scroll.
- */
- final class PendingMoveSmoothScroller extends GridLinearSmoothScroller {
- // -2 is a target position that LinearSmoothScroller can never find until
- // consumePendingMovesXXX() sets real targetPosition.
- final static int TARGET_UNDEFINED = -2;
- // whether the grid is staggered.
- private final boolean mStaggeredGrid;
- // Number of pending movements on primary direction, negative if PREV_ITEM.
- private int mPendingMoves;
-
- PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid) {
- mPendingMoves = initialPendingMoves;
- mStaggeredGrid = staggeredGrid;
- setTargetPosition(TARGET_UNDEFINED);
- }
-
- void increasePendingMoves() {
- if (mPendingMoves < mMaxPendingMoves) {
- mPendingMoves++;
- }
- }
-
- void decreasePendingMoves() {
- if (mPendingMoves > -mMaxPendingMoves) {
- mPendingMoves--;
- }
- }
-
- /**
- * Called before laid out an item when non-staggered grid can handle pending movements
- * by skipping "mNumRows" per movement; staggered grid will have to wait the item
- * has been laid out in consumePendingMovesAfterLayout().
- */
- void consumePendingMovesBeforeLayout() {
- if (mStaggeredGrid || mPendingMoves == 0) {
- return;
- }
- View newSelected = null;
- int startPos = mPendingMoves > 0 ? mFocusPosition + mNumRows :
- mFocusPosition - mNumRows;
- for (int pos = startPos; mPendingMoves != 0;
- pos = mPendingMoves > 0 ? pos + mNumRows: pos - mNumRows) {
- View v = findViewByPosition(pos);
- if (v == null) {
- break;
- }
- if (!canScrollTo(v)) {
- continue;
- }
- newSelected = v;
- mFocusPosition = pos;
- mSubFocusPosition = 0;
- if (mPendingMoves > 0) {
- mPendingMoves--;
- } else {
- mPendingMoves++;
- }
- }
- if (newSelected != null && hasFocus()) {
- mInSelection = true;
- newSelected.requestFocus();
- mInSelection = false;
- }
- }
-
- /**
- * Called after laid out an item. Staggered grid should find view on same
- * Row and consume pending movements.
- */
- void consumePendingMovesAfterLayout() {
- if (mStaggeredGrid && mPendingMoves != 0) {
- // consume pending moves, focus to item on the same row.
- mPendingMoves = processSelectionMoves(true, mPendingMoves);
- }
- if (mPendingMoves == 0 || (mPendingMoves > 0 && hasCreatedLastItem())
- || (mPendingMoves < 0 && hasCreatedFirstItem())) {
- setTargetPosition(mFocusPosition);
- stop();
- }
- }
-
- @Override
- protected void updateActionForInterimTarget(Action action) {
- if (mPendingMoves == 0) {
- return;
- }
- super.updateActionForInterimTarget(action);
- }
-
- @Override
- public PointF computeScrollVectorForPosition(int targetPosition) {
- if (mPendingMoves == 0) {
- return null;
- }
- int direction = (mReverseFlowPrimary ? mPendingMoves > 0 : mPendingMoves < 0)
- ? -1 : 1;
- if (mOrientation == HORIZONTAL) {
- return new PointF(direction, 0);
- } else {
- return new PointF(0, direction);
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- // if we hit wall, need clear the remaining pending moves.
- mPendingMoves = 0;
- mPendingMoveSmoothScroller = null;
- View v = findViewByPosition(getTargetPosition());
- if (v != null) scrollToView(v, true);
- }
- };
-
- private static final String TAG = "GridLayoutManager";
- static final boolean DEBUG = false;
- static final boolean TRACE = false;
-
- // maximum pending movement in one direction.
- static final int DEFAULT_MAX_PENDING_MOVES = 10;
- int mMaxPendingMoves = DEFAULT_MAX_PENDING_MOVES;
- // minimal milliseconds to scroll window size in major direction, we put a cap to prevent the
- // effect smooth scrolling too over to bind an item view then drag the item view back.
- final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
-
- // Represents whether child views are temporarily sliding out
- boolean mIsSlidingChildViews;
- boolean mLayoutEatenInSliding;
-
- String getTag() {
- return TAG + ":" + mBaseGridView.getId();
- }
-
- final BaseGridView mBaseGridView;
-
- /**
- * Note on conventions in the presence of RTL layout directions:
- * Many properties and method names reference entities related to the
- * beginnings and ends of things. In the presence of RTL flows,
- * it may not be clear whether this is intended to reference a
- * quantity that changes direction in RTL cases, or a quantity that
- * does not. Here are the conventions in use:
- *
- * start/end: coordinate quantities - do reverse
- * (optical) left/right: coordinate quantities - do not reverse
- * low/high: coordinate quantities - do not reverse
- * min/max: coordinate quantities - do not reverse
- * scroll offset - coordinate quantities - do not reverse
- * first/last: positional indices - do not reverse
- * front/end: positional indices - do not reverse
- * prepend/append: related to positional indices - do not reverse
- *
- * Note that although quantities do not reverse in RTL flows, their
- * relationship does. In LTR flows, the first positional index is
- * leftmost; in RTL flows, it is rightmost. Thus, anywhere that
- * positional quantities are mapped onto coordinate quantities,
- * the flow must be checked and the logic reversed.
- */
-
- /**
- * The orientation of a "row".
- */
- @RecyclerView.Orientation
- int mOrientation = HORIZONTAL;
- private OrientationHelper mOrientationHelper = OrientationHelper.createHorizontalHelper(this);
-
- RecyclerView.State mState;
- // Suppose currently showing 4, 5, 6, 7; removing 2,3,4 will make the layoutPosition to be
- // 2(deleted), 3, 4, 5 in prelayout pass. So when we add item in prelayout, we must subtract 2
- // from index of Grid.createItem.
- int mPositionDeltaInPreLayout;
- // Extra layout space needs to fill in prelayout pass. Note we apply the extra space to both
- // appends and prepends due to the fact leanback is doing mario scrolling: removing items to
- // the left of focused item might need extra layout on the right.
- int mExtraLayoutSpaceInPreLayout;
- // mPositionToRowInPostLayout and mDisappearingPositions are temp variables in post layout.
- final SparseIntArray mPositionToRowInPostLayout = new SparseIntArray();
- int[] mDisappearingPositions;
-
- RecyclerView.Recycler mRecycler;
-
- private static final Rect sTempRect = new Rect();
-
- boolean mInLayout;
- private boolean mInScroll;
- boolean mInFastRelayout;
- /**
- * During full layout pass, when GridView had focus: onLayoutChildren will
- * skip non-focusable child and adjust mFocusPosition.
- */
- boolean mInLayoutSearchFocus;
- boolean mInSelection = false;
-
- private OnChildSelectedListener mChildSelectedListener = null;
-
- private ArrayList<OnChildViewHolderSelectedListener> mChildViewHolderSelectedListeners = null;
-
- OnChildLaidOutListener mChildLaidOutListener = null;
-
- /**
- * The focused position, it's not the currently visually aligned position
- * but it is the final position that we intend to focus on. If there are
- * multiple setSelection() called, mFocusPosition saves last value.
- */
- int mFocusPosition = NO_POSITION;
-
- /**
- * A view can have multiple alignment position, this is the index of which
- * alignment is used, by default is 0.
- */
- int mSubFocusPosition = 0;
-
- /**
- * LinearSmoothScroller that consume pending DPAD movements.
- */
- PendingMoveSmoothScroller mPendingMoveSmoothScroller;
-
- /**
- * The offset to be applied to mFocusPosition, due to adapter change, on the next
- * layout. Set to Integer.MIN_VALUE means we should stop adding delta to mFocusPosition
- * until next layout cycler.
- * TODO: This is somewhat duplication of RecyclerView getOldPosition() which is
- * unfortunately cleared after prelayout.
- */
- private int mFocusPositionOffset = 0;
-
- /**
- * Extra pixels applied on primary direction.
- */
- private int mPrimaryScrollExtra;
-
- /**
- * Force a full layout under certain situations. E.g. Rows change, jump to invisible child.
- */
- private boolean mForceFullLayout;
-
- /**
- * True if layout is enabled.
- */
- private boolean mLayoutEnabled = true;
-
- /**
- * override child visibility
- */
- @Visibility
- int mChildVisibility;
-
- /**
- * Pixels that scrolled in secondary forward direction. Negative value means backward.
- * Note that we treat secondary differently than main. For the main axis, update scroll min/max
- * based on first/last item's view location. For second axis, we don't use item's view location.
- * We are using the {@link #getRowSizeSecondary(int)} plus mScrollOffsetSecondary. see
- * details in {@link #updateSecondaryScrollLimits()}.
- */
- int mScrollOffsetSecondary;
-
- /**
- * User-specified row height/column width. Can be WRAP_CONTENT.
- */
- private int mRowSizeSecondaryRequested;
-
- /**
- * The fixed size of each grid item in the secondary direction. This corresponds to
- * the row height, equal for all rows. Grid items may have variable length
- * in the primary direction.
- */
- private int mFixedRowSizeSecondary;
-
- /**
- * Tracks the secondary size of each row.
- */
- private int[] mRowSizeSecondary;
-
- /**
- * Flag controlling whether the current/next layout should
- * be updating the secondary size of rows.
- */
- private boolean mRowSecondarySizeRefresh;
-
- /**
- * The maximum measured size of the view.
- */
- private int mMaxSizeSecondary;
-
- /**
- * Margin between items.
- */
- private int mHorizontalSpacing;
- /**
- * Margin between items vertically.
- */
- private int mVerticalSpacing;
- /**
- * Margin in main direction.
- */
- private int mSpacingPrimary;
- /**
- * Margin in second direction.
- */
- private int mSpacingSecondary;
- /**
- * How to position child in secondary direction.
- */
- private int mGravity = Gravity.START | Gravity.TOP;
- /**
- * The number of rows in the grid.
- */
- int mNumRows;
- /**
- * Number of rows requested, can be 0 to be determined by parent size and
- * rowHeight.
- */
- private int mNumRowsRequested = 1;
-
- /**
- * Saves grid information of each view.
- */
- Grid mGrid;
-
- /**
- * Focus Scroll strategy.
- */
- private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
- /**
- * Defines how item view is aligned in the window.
- */
- final WindowAlignment mWindowAlignment = new WindowAlignment();
-
- /**
- * Defines how item view is aligned.
- */
- private final ItemAlignment mItemAlignment = new ItemAlignment();
-
- /**
- * Dimensions of the view, width or height depending on orientation.
- */
- private int mSizePrimary;
-
- /**
- * Pixels of extra space for layout item (outside the widget)
- */
- private int mExtraLayoutSpace;
-
- /**
- * Allow DPAD key to navigate out at the front of the View (where position = 0),
- * default is false.
- */
- private boolean mFocusOutFront;
-
- /**
- * Allow DPAD key to navigate out at the end of the view, default is false.
- */
- private boolean mFocusOutEnd;
-
- /**
- * Allow DPAD key to navigate out of second axis.
- * default is true.
- */
- private boolean mFocusOutSideStart = true;
-
- /**
- * Allow DPAD key to navigate out of second axis.
- */
- private boolean mFocusOutSideEnd = true;
-
- /**
- * True if focus search is disabled.
- */
- private boolean mFocusSearchDisabled;
-
- /**
- * True if prune child, might be disabled during transition.
- */
- private boolean mPruneChild = true;
-
- /**
- * True if scroll content, might be disabled during transition.
- */
- private boolean mScrollEnabled = true;
-
- /**
- * Temporary variable: an int array of length=2.
- */
- static int[] sTwoInts = new int[2];
-
- /**
- * Set to true for RTL layout in horizontal orientation
- */
- boolean mReverseFlowPrimary = false;
-
- /**
- * Set to true for RTL layout in vertical orientation
- */
- private boolean mReverseFlowSecondary = false;
-
- /**
- * Temporaries used for measuring.
- */
- private int[] mMeasuredDimension = new int[2];
-
- final ViewsStateBundle mChildrenStates = new ViewsStateBundle();
-
- /**
- * Optional interface implemented by Adapter.
- */
- private FacetProviderAdapter mFacetProviderAdapter;
-
- public GridLayoutManager(BaseGridView baseGridView) {
- mBaseGridView = baseGridView;
- mChildVisibility = -1;
- // disable prefetch by default, prefetch causes regression on low power chipset
- setItemPrefetchEnabled(false);
- }
-
- public void setOrientation(@RecyclerView.Orientation int orientation) {
- if (orientation != HORIZONTAL && orientation != VERTICAL) {
- if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
- return;
- }
-
- mOrientation = orientation;
- mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
- mWindowAlignment.setOrientation(orientation);
- mItemAlignment.setOrientation(orientation);
- mForceFullLayout = true;
- }
-
- public void onRtlPropertiesChanged(int layoutDirection) {
- boolean reversePrimary, reverseSecondary;
- if (mOrientation == HORIZONTAL) {
- reversePrimary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
- reverseSecondary = false;
- } else {
- reverseSecondary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
- reversePrimary = false;
- }
- if (mReverseFlowPrimary == reversePrimary && mReverseFlowSecondary == reverseSecondary) {
- return;
- }
- mReverseFlowPrimary = reversePrimary;
- mReverseFlowSecondary = reverseSecondary;
- mForceFullLayout = true;
- mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
- }
-
- public int getFocusScrollStrategy() {
- return mFocusScrollStrategy;
- }
-
- public void setFocusScrollStrategy(int focusScrollStrategy) {
- mFocusScrollStrategy = focusScrollStrategy;
- }
-
- public void setWindowAlignment(int windowAlignment) {
- mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
- }
-
- public int getWindowAlignment() {
- return mWindowAlignment.mainAxis().getWindowAlignment();
- }
-
- public void setWindowAlignmentOffset(int alignmentOffset) {
- mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
- }
-
- public int getWindowAlignmentOffset() {
- return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
- }
-
- public void setWindowAlignmentOffsetPercent(float offsetPercent) {
- mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
- }
-
- public float getWindowAlignmentOffsetPercent() {
- return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
- }
-
- public void setItemAlignmentOffset(int alignmentOffset) {
- mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
- updateChildAlignments();
- }
-
- public int getItemAlignmentOffset() {
- return mItemAlignment.mainAxis().getItemAlignmentOffset();
- }
-
- public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
- mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
- updateChildAlignments();
- }
-
- public boolean isItemAlignmentOffsetWithPadding() {
- return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
- }
-
- public void setItemAlignmentOffsetPercent(float offsetPercent) {
- mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
- updateChildAlignments();
- }
-
- public float getItemAlignmentOffsetPercent() {
- return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
- }
-
- public void setItemAlignmentViewId(int viewId) {
- mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
- updateChildAlignments();
- }
-
- public int getItemAlignmentViewId() {
- return mItemAlignment.mainAxis().getItemAlignmentViewId();
- }
-
- public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
- mFocusOutFront = throughFront;
- mFocusOutEnd = throughEnd;
- }
-
- public void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) {
- mFocusOutSideStart = throughStart;
- mFocusOutSideEnd = throughEnd;
- }
-
- public void setNumRows(int numRows) {
- if (numRows < 0) throw new IllegalArgumentException();
- mNumRowsRequested = numRows;
- }
-
- /**
- * Set the row height. May be WRAP_CONTENT, or a size in pixels.
- */
- public void setRowHeight(int height) {
- if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
- mRowSizeSecondaryRequested = height;
- } else {
- throw new IllegalArgumentException("Invalid row height: " + height);
- }
- }
-
- public void setItemSpacing(int space) {
- mVerticalSpacing = mHorizontalSpacing = space;
- mSpacingPrimary = mSpacingSecondary = space;
- }
-
- public void setVerticalSpacing(int space) {
- if (mOrientation == VERTICAL) {
- mSpacingPrimary = mVerticalSpacing = space;
- } else {
- mSpacingSecondary = mVerticalSpacing = space;
- }
- }
-
- public void setHorizontalSpacing(int space) {
- if (mOrientation == HORIZONTAL) {
- mSpacingPrimary = mHorizontalSpacing = space;
- } else {
- mSpacingSecondary = mHorizontalSpacing = space;
- }
- }
-
- public int getVerticalSpacing() {
- return mVerticalSpacing;
- }
-
- public int getHorizontalSpacing() {
- return mHorizontalSpacing;
- }
-
- public void setGravity(int gravity) {
- mGravity = gravity;
- }
-
- protected boolean hasDoneFirstLayout() {
- return mGrid != null;
- }
-
- public void setOnChildSelectedListener(OnChildSelectedListener listener) {
- mChildSelectedListener = listener;
- }
-
- public void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
- if (listener == null) {
- mChildViewHolderSelectedListeners = null;
- return;
- }
- if (mChildViewHolderSelectedListeners == null) {
- mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>();
- } else {
- mChildViewHolderSelectedListeners.clear();
- }
- mChildViewHolderSelectedListeners.add(listener);
- }
-
- public void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
- if (mChildViewHolderSelectedListeners == null) {
- mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>();
- }
- mChildViewHolderSelectedListeners.add(listener);
- }
-
- public void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener
- listener) {
- if (mChildViewHolderSelectedListeners != null) {
- mChildViewHolderSelectedListeners.remove(listener);
- }
- }
-
- boolean hasOnChildViewHolderSelectedListener() {
- return mChildViewHolderSelectedListeners != null
- && mChildViewHolderSelectedListeners.size() > 0;
- }
-
- void fireOnChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child,
- int position, int subposition) {
- if (mChildViewHolderSelectedListeners == null) {
- return;
- }
- for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0 ; i--) {
- mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelected(parent, child,
- position, subposition);
- }
- }
-
- void fireOnChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder
- child, int position, int subposition) {
- if (mChildViewHolderSelectedListeners == null) {
- return;
- }
- for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0 ; i--) {
- mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelectedAndPositioned(parent,
- child, position, subposition);
- }
- }
-
- void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
- mChildLaidOutListener = listener;
- }
-
- private int getAdapterPositionByView(View view) {
- if (view == null) {
- return NO_POSITION;
- }
- LayoutParams params = (LayoutParams) view.getLayoutParams();
- if (params == null || params.isItemRemoved()) {
- // when item is removed, the position value can be any value.
- return NO_POSITION;
- }
- return params.getViewAdapterPosition();
- }
-
- int getSubPositionByView(View view, View childView) {
- if (view == null || childView == null) {
- return 0;
- }
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- final ItemAlignmentFacet facet = lp.getItemAlignmentFacet();
- if (facet != null) {
- final ItemAlignmentFacet.ItemAlignmentDef[] defs = facet.getAlignmentDefs();
- if (defs.length > 1) {
- while (childView != view) {
- int id = childView.getId();
- if (id != View.NO_ID) {
- for (int i = 1; i < defs.length; i++) {
- if (defs[i].getItemAlignmentFocusViewId() == id) {
- return i;
- }
- }
- }
- childView = (View) childView.getParent();
- }
- }
- }
- return 0;
- }
-
- private int getAdapterPositionByIndex(int index) {
- return getAdapterPositionByView(getChildAt(index));
- }
-
- void dispatchChildSelected() {
- if (mChildSelectedListener == null && !hasOnChildViewHolderSelectedListener()) {
- return;
- }
-
- if (TRACE) TraceCompat.beginSection("onChildSelected");
- View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
- if (view != null) {
- RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
- if (mChildSelectedListener != null) {
- mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
- vh == null? NO_ID: vh.getItemId());
- }
- fireOnChildViewHolderSelected(mBaseGridView, vh, mFocusPosition, mSubFocusPosition);
- } else {
- if (mChildSelectedListener != null) {
- mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
- }
- fireOnChildViewHolderSelected(mBaseGridView, null, NO_POSITION, 0);
- }
- if (TRACE) TraceCompat.endSection();
-
- // Children may request layout when a child selection event occurs (such as a change of
- // padding on the current and previously selected rows).
- // If in layout, a child requesting layout may have been laid out before the selection
- // callback.
- // If it was not, the child will be laid out after the selection callback.
- // If so, the layout request will be honoured though the view system will emit a double-
- // layout warning.
- // If not in layout, we may be scrolling in which case the child layout request will be
- // eaten by recyclerview. Post a requestLayout.
- if (!mInLayout && !mBaseGridView.isLayoutRequested()) {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- if (getChildAt(i).isLayoutRequested()) {
- forceRequestLayout();
- break;
- }
- }
- }
- }
-
- private void dispatchChildSelectedAndPositioned() {
- if (!hasOnChildViewHolderSelectedListener()) {
- return;
- }
-
- if (TRACE) TraceCompat.beginSection("onChildSelectedAndPositioned");
- View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
- if (view != null) {
- RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
- fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, vh, mFocusPosition,
- mSubFocusPosition);
- } else {
- if (mChildSelectedListener != null) {
- mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
- }
- fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, null, NO_POSITION, 0);
- }
- if (TRACE) TraceCompat.endSection();
-
- }
-
- @Override
- public boolean canScrollHorizontally() {
- // We can scroll horizontally if we have horizontal orientation, or if
- // we are vertical and have more than one column.
- return mOrientation == HORIZONTAL || mNumRows > 1;
- }
-
- @Override
- public boolean canScrollVertically() {
- // We can scroll vertically if we have vertical orientation, or if we
- // are horizontal and have more than one row.
- return mOrientation == VERTICAL || mNumRows > 1;
- }
-
- @Override
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- }
-
- @Override
- public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
- return new LayoutParams(context, attrs);
- }
-
- @Override
- public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
- if (lp instanceof LayoutParams) {
- return new LayoutParams((LayoutParams) lp);
- } else if (lp instanceof RecyclerView.LayoutParams) {
- return new LayoutParams((RecyclerView.LayoutParams) lp);
- } else if (lp instanceof MarginLayoutParams) {
- return new LayoutParams((MarginLayoutParams) lp);
- } else {
- return new LayoutParams(lp);
- }
- }
-
- protected View getViewForPosition(int position) {
- return mRecycler.getViewForPosition(position);
- }
-
- final int getOpticalLeft(View v) {
- return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
- }
-
- final int getOpticalRight(View v) {
- return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
- }
-
- final int getOpticalTop(View v) {
- return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
- }
-
- final int getOpticalBottom(View v) {
- return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
- }
-
- @Override
- public int getDecoratedLeft(View child) {
- return super.getDecoratedLeft(child) + ((LayoutParams) child.getLayoutParams()).mLeftInset;
- }
-
- @Override
- public int getDecoratedTop(View child) {
- return super.getDecoratedTop(child) + ((LayoutParams) child.getLayoutParams()).mTopInset;
- }
-
- @Override
- public int getDecoratedRight(View child) {
- return super.getDecoratedRight(child)
- - ((LayoutParams) child.getLayoutParams()).mRightInset;
- }
-
- @Override
- public int getDecoratedBottom(View child) {
- return super.getDecoratedBottom(child)
- - ((LayoutParams) child.getLayoutParams()).mBottomInset;
- }
-
- @Override
- public void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
- super.getDecoratedBoundsWithMargins(view, outBounds);
- LayoutParams params = ((LayoutParams) view.getLayoutParams());
- outBounds.left += params.mLeftInset;
- outBounds.top += params.mTopInset;
- outBounds.right -= params.mRightInset;
- outBounds.bottom -= params.mBottomInset;
- }
-
- int getViewMin(View v) {
- return mOrientationHelper.getDecoratedStart(v);
- }
-
- int getViewMax(View v) {
- return mOrientationHelper.getDecoratedEnd(v);
- }
-
- int getViewPrimarySize(View view) {
- getDecoratedBoundsWithMargins(view, sTempRect);
- return mOrientation == HORIZONTAL ? sTempRect.width() : sTempRect.height();
- }
-
- private int getViewCenter(View view) {
- return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
- }
-
- private int getAdjustedViewCenter(View view) {
- if (view.hasFocus()) {
- View child = view.findFocus();
- if (child != null && child != view) {
- return getAdjustedPrimaryAlignedScrollDistance(getViewCenter(view), view, child);
- }
- }
- return getViewCenter(view);
- }
-
- private int getViewCenterSecondary(View view) {
- return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
- }
-
- private int getViewCenterX(View v) {
- LayoutParams p = (LayoutParams) v.getLayoutParams();
- return p.getOpticalLeft(v) + p.getAlignX();
- }
-
- private int getViewCenterY(View v) {
- LayoutParams p = (LayoutParams) v.getLayoutParams();
- return p.getOpticalTop(v) + p.getAlignY();
- }
-
- /**
- * Save Recycler and State for convenience. Must be paired with leaveContext().
- */
- private void saveContext(Recycler recycler, State state) {
- if (mRecycler != null || mState != null) {
- Log.e(TAG, "Recycler information was not released, bug!");
- }
- mRecycler = recycler;
- mState = state;
- mPositionDeltaInPreLayout = 0;
- mExtraLayoutSpaceInPreLayout = 0;
- }
-
- /**
- * Discard saved Recycler and State.
- */
- private void leaveContext() {
- mRecycler = null;
- mState = null;
- mPositionDeltaInPreLayout = 0;
- mExtraLayoutSpaceInPreLayout = 0;
- }
-
- /**
- * Re-initialize data structures for a data change or handling invisible
- * selection. The method tries its best to preserve position information so
- * that staggered grid looks same before and after re-initialize.
- * @return true if can fastRelayout()
- */
- private boolean layoutInit() {
- final int newItemCount = mState.getItemCount();
- if (newItemCount == 0) {
- mFocusPosition = NO_POSITION;
- mSubFocusPosition = 0;
- } else if (mFocusPosition >= newItemCount) {
- mFocusPosition = newItemCount - 1;
- mSubFocusPosition = 0;
- } else if (mFocusPosition == NO_POSITION && newItemCount > 0) {
- // if focus position is never set before, initialize it to 0
- mFocusPosition = 0;
- mSubFocusPosition = 0;
- }
- if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
- && !mForceFullLayout && mGrid.getNumRows() == mNumRows) {
- updateScrollController();
- updateSecondaryScrollLimits();
- mGrid.setSpacing(mSpacingPrimary);
- return true;
- } else {
- mForceFullLayout = false;
-
- if (mGrid == null || mNumRows != mGrid.getNumRows()
- || mReverseFlowPrimary != mGrid.isReversedFlow()) {
- mGrid = Grid.createGrid(mNumRows);
- mGrid.setProvider(mGridProvider);
- mGrid.setReversedFlow(mReverseFlowPrimary);
- }
- initScrollController();
- updateSecondaryScrollLimits();
- mGrid.setSpacing(mSpacingPrimary);
- detachAndScrapAttachedViews(mRecycler);
- mGrid.resetVisibleIndex();
- mWindowAlignment.mainAxis().invalidateScrollMin();
- mWindowAlignment.mainAxis().invalidateScrollMax();
- return false;
- }
- }
-
- private int getRowSizeSecondary(int rowIndex) {
- if (mFixedRowSizeSecondary != 0) {
- return mFixedRowSizeSecondary;
- }
- if (mRowSizeSecondary == null) {
- return 0;
- }
- return mRowSizeSecondary[rowIndex];
- }
-
- int getRowStartSecondary(int rowIndex) {
- int start = 0;
- // Iterate from left to right, which is a different index traversal
- // in RTL flow
- if (mReverseFlowSecondary) {
- for (int i = mNumRows-1; i > rowIndex; i--) {
- start += getRowSizeSecondary(i) + mSpacingSecondary;
- }
- } else {
- for (int i = 0; i < rowIndex; i++) {
- start += getRowSizeSecondary(i) + mSpacingSecondary;
- }
- }
- return start;
- }
-
- private int getSizeSecondary() {
- int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1;
- return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
- }
-
- int getDecoratedMeasuredWidthWithMargin(View v) {
- final LayoutParams lp = (LayoutParams) v.getLayoutParams();
- return getDecoratedMeasuredWidth(v) + lp.leftMargin + lp.rightMargin;
- }
-
- int getDecoratedMeasuredHeightWithMargin(View v) {
- final LayoutParams lp = (LayoutParams) v.getLayoutParams();
- return getDecoratedMeasuredHeight(v) + lp.topMargin + lp.bottomMargin;
- }
-
- private void measureScrapChild(int position, int widthSpec, int heightSpec,
- int[] measuredDimension) {
- View view = mRecycler.getViewForPosition(position);
- if (view != null) {
- final LayoutParams p = (LayoutParams) view.getLayoutParams();
- calculateItemDecorationsForChild(view, sTempRect);
- int widthUsed = p.leftMargin + p.rightMargin + sTempRect.left + sTempRect.right;
- int heightUsed = p.topMargin + p.bottomMargin + sTempRect.top + sTempRect.bottom;
-
- int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
- getPaddingLeft() + getPaddingRight() + widthUsed, p.width);
- int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
- getPaddingTop() + getPaddingBottom() + heightUsed, p.height);
- view.measure(childWidthSpec, childHeightSpec);
-
- measuredDimension[0] = getDecoratedMeasuredWidthWithMargin(view);
- measuredDimension[1] = getDecoratedMeasuredHeightWithMargin(view);
- mRecycler.recycleView(view);
- }
- }
-
- private boolean processRowSizeSecondary(boolean measure) {
- if (mFixedRowSizeSecondary != 0 || mRowSizeSecondary == null) {
- return false;
- }
-
- if (TRACE) TraceCompat.beginSection("processRowSizeSecondary");
- CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
- boolean changed = false;
- int scrapeChildSize = -1;
-
- for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
- CircularIntArray row = rows == null ? null : rows[rowIndex];
- final int rowItemsPairCount = row == null ? 0 : row.size();
- int rowSize = -1;
- for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
- rowItemPairIndex += 2) {
- final int rowIndexStart = row.get(rowItemPairIndex);
- final int rowIndexEnd = row.get(rowItemPairIndex + 1);
- for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
- final View view = findViewByPosition(i - mPositionDeltaInPreLayout);
- if (view == null) {
- continue;
- }
- if (measure) {
- measureChild(view);
- }
- final int secondarySize = mOrientation == HORIZONTAL
- ? getDecoratedMeasuredHeightWithMargin(view)
- : getDecoratedMeasuredWidthWithMargin(view);
- if (secondarySize > rowSize) {
- rowSize = secondarySize;
- }
- }
- }
-
- final int itemCount = mState.getItemCount();
- if (!mBaseGridView.hasFixedSize() && measure && rowSize < 0 && itemCount > 0) {
- if (scrapeChildSize < 0) {
- // measure a child that is close to mFocusPosition but not currently visible
- int position = mFocusPosition;
- if (position < 0) {
- position = 0;
- } else if (position >= itemCount) {
- position = itemCount - 1;
- }
- if (getChildCount() > 0) {
- int firstPos = mBaseGridView.getChildViewHolder(
- getChildAt(0)).getLayoutPosition();
- int lastPos = mBaseGridView.getChildViewHolder(
- getChildAt(getChildCount() - 1)).getLayoutPosition();
- // if mFocusPosition is between first and last, choose either
- // first - 1 or last + 1
- if (position >= firstPos && position <= lastPos) {
- position = (position - firstPos <= lastPos - position)
- ? (firstPos - 1) : (lastPos + 1);
- // try the other value if the position is invalid. if both values are
- // invalid, skip measureScrapChild below.
- if (position < 0 && lastPos < itemCount - 1) {
- position = lastPos + 1;
- } else if (position >= itemCount && firstPos > 0) {
- position = firstPos - 1;
- }
- }
- }
- if (position >= 0 && position < itemCount) {
- measureScrapChild(position,
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- mMeasuredDimension);
- scrapeChildSize = mOrientation == HORIZONTAL ? mMeasuredDimension[1] :
- mMeasuredDimension[0];
- if (DEBUG) {
- Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] + " "
- + mMeasuredDimension[1]);
- }
- }
- }
- if (scrapeChildSize >= 0) {
- rowSize = scrapeChildSize;
- }
- }
- if (rowSize < 0) {
- rowSize = 0;
- }
- if (mRowSizeSecondary[rowIndex] != rowSize) {
- if (DEBUG) {
- Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex]
- + ", " + rowSize);
- }
- mRowSizeSecondary[rowIndex] = rowSize;
- changed = true;
- }
- }
-
- if (TRACE) TraceCompat.endSection();
- return changed;
- }
-
- /**
- * Checks if we need to update row secondary sizes.
- */
- private void updateRowSecondarySizeRefresh() {
- mRowSecondarySizeRefresh = processRowSizeSecondary(false);
- if (mRowSecondarySizeRefresh) {
- if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
- forceRequestLayout();
- }
- }
-
- private void forceRequestLayout() {
- if (DEBUG) Log.v(getTag(), "forceRequestLayout");
- // RecyclerView prevents us from requesting layout in many cases
- // (during layout, during scroll, etc.)
- // For secondary row size wrap_content support we currently need a
- // second layout pass to update the measured size after having measured
- // and added child views in layoutChildren.
- // Force the second layout by posting a delayed runnable.
- // TODO: investigate allowing a second layout pass,
- // or move child add/measure logic to the measure phase.
- ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
- }
-
- private final Runnable mRequestLayoutRunnable = new Runnable() {
- @Override
- public void run() {
- if (DEBUG) Log.v(getTag(), "request Layout from runnable");
- requestLayout();
- }
- };
-
- @Override
- public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
- saveContext(recycler, state);
-
- int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
- int measuredSizeSecondary;
- if (mOrientation == HORIZONTAL) {
- sizePrimary = MeasureSpec.getSize(widthSpec);
- sizeSecondary = MeasureSpec.getSize(heightSpec);
- modeSecondary = MeasureSpec.getMode(heightSpec);
- paddingSecondary = getPaddingTop() + getPaddingBottom();
- } else {
- sizeSecondary = MeasureSpec.getSize(widthSpec);
- sizePrimary = MeasureSpec.getSize(heightSpec);
- modeSecondary = MeasureSpec.getMode(widthSpec);
- paddingSecondary = getPaddingLeft() + getPaddingRight();
- }
- if (DEBUG) {
- Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec)
- + " heightSpec " + Integer.toHexString(heightSpec)
- + " modeSecondary " + Integer.toHexString(modeSecondary)
- + " sizeSecondary " + sizeSecondary + " " + this);
- }
-
- mMaxSizeSecondary = sizeSecondary;
-
- if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
- mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
- mFixedRowSizeSecondary = 0;
-
- if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
- mRowSizeSecondary = new int[mNumRows];
- }
-
- if (mState.isPreLayout()) {
- updatePositionDeltaInPreLayout();
- }
- // Measure all current children and update cached row height or column width
- processRowSizeSecondary(true);
-
- switch (modeSecondary) {
- case MeasureSpec.UNSPECIFIED:
- measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
- break;
- case MeasureSpec.AT_MOST:
- measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
- mMaxSizeSecondary);
- break;
- case MeasureSpec.EXACTLY:
- measuredSizeSecondary = mMaxSizeSecondary;
- break;
- default:
- throw new IllegalStateException("wrong spec");
- }
-
- } else {
- switch (modeSecondary) {
- case MeasureSpec.UNSPECIFIED:
- mFixedRowSizeSecondary = mRowSizeSecondaryRequested == 0
- ? sizeSecondary - paddingSecondary : mRowSizeSecondaryRequested;
- mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
- measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
- * (mNumRows - 1) + paddingSecondary;
- break;
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
- mNumRows = 1;
- mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
- } else if (mNumRowsRequested == 0) {
- mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
- mNumRows = (sizeSecondary + mSpacingSecondary)
- / (mRowSizeSecondaryRequested + mSpacingSecondary);
- } else if (mRowSizeSecondaryRequested == 0) {
- mNumRows = mNumRowsRequested;
- mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary
- - mSpacingSecondary * (mNumRows - 1)) / mNumRows;
- } else {
- mNumRows = mNumRowsRequested;
- mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
- }
- measuredSizeSecondary = sizeSecondary;
- if (modeSecondary == MeasureSpec.AT_MOST) {
- int childrenSize = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
- * (mNumRows - 1) + paddingSecondary;
- if (childrenSize < measuredSizeSecondary) {
- measuredSizeSecondary = childrenSize;
- }
- }
- break;
- default:
- throw new IllegalStateException("wrong spec");
- }
- }
- if (mOrientation == HORIZONTAL) {
- setMeasuredDimension(sizePrimary, measuredSizeSecondary);
- } else {
- setMeasuredDimension(measuredSizeSecondary, sizePrimary);
- }
- if (DEBUG) {
- Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary
- + " measuredSizeSecondary " + measuredSizeSecondary
- + " mFixedRowSizeSecondary " + mFixedRowSizeSecondary
- + " mNumRows " + mNumRows);
- }
- leaveContext();
- }
-
- void measureChild(View child) {
- if (TRACE) TraceCompat.beginSection("measureChild");
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- calculateItemDecorationsForChild(child, sTempRect);
- int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right;
- int heightUsed = lp.topMargin + lp.bottomMargin + sTempRect.top + sTempRect.bottom;
-
- final int secondarySpec =
- (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT)
- ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
- : MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
- int widthSpec, heightSpec;
-
- if (mOrientation == HORIZONTAL) {
- widthSpec = ViewGroup.getChildMeasureSpec(
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), widthUsed, lp.width);
- heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, heightUsed, lp.height);
- } else {
- heightSpec = ViewGroup.getChildMeasureSpec(
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed, lp.height);
- widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, widthUsed, lp.width);
- }
- child.measure(widthSpec, heightSpec);
- if (DEBUG) {
- Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec)
- + " widthSpec " + Integer.toHexString(widthSpec)
- + " heightSpec " + Integer.toHexString(heightSpec)
- + " measuredWidth " + child.getMeasuredWidth()
- + " measuredHeight " + child.getMeasuredHeight());
- }
- if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
- if (TRACE) TraceCompat.endSection();
- }
-
- /**
- * Get facet from the ViewHolder or the viewType.
- */
- <E> E getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass) {
- E facet = null;
- if (vh instanceof FacetProvider) {
- facet = (E) ((FacetProvider) vh).getFacet(facetClass);
- }
- if (facet == null && mFacetProviderAdapter != null) {
- FacetProvider p = mFacetProviderAdapter.getFacetProvider(vh.getItemViewType());
- if (p != null) {
- facet = (E) p.getFacet(facetClass);
- }
- }
- return facet;
- }
-
- private Grid.Provider mGridProvider = new Grid.Provider() {
-
- @Override
- public int getMinIndex() {
- return mPositionDeltaInPreLayout;
- }
-
- @Override
- public int getCount() {
- return mState.getItemCount() + mPositionDeltaInPreLayout;
- }
-
- @Override
- public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) {
- if (TRACE) TraceCompat.beginSection("createItem");
- if (TRACE) TraceCompat.beginSection("getview");
- View v = getViewForPosition(index - mPositionDeltaInPreLayout);
- if (TRACE) TraceCompat.endSection();
- LayoutParams lp = (LayoutParams) v.getLayoutParams();
- RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
- lp.setItemAlignmentFacet((ItemAlignmentFacet)getFacet(vh, ItemAlignmentFacet.class));
- // See recyclerView docs: we don't need re-add scraped view if it was removed.
- if (!lp.isItemRemoved()) {
- if (TRACE) TraceCompat.beginSection("addView");
- if (disappearingItem) {
- if (append) {
- addDisappearingView(v);
- } else {
- addDisappearingView(v, 0);
- }
- } else {
- if (append) {
- addView(v);
- } else {
- addView(v, 0);
- }
- }
- if (TRACE) TraceCompat.endSection();
- if (mChildVisibility != -1) {
- v.setVisibility(mChildVisibility);
- }
-
- if (mPendingMoveSmoothScroller != null) {
- mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
- }
- int subindex = getSubPositionByView(v, v.findFocus());
- if (!mInLayout) {
- // when we are appending item during scroll pass and the item's position
- // matches the mFocusPosition, we should signal a childSelected event.
- // However if we are still running PendingMoveSmoothScroller, we defer and
- // signal the event in PendingMoveSmoothScroller.onStop(). This can
- // avoid lots of childSelected events during a long smooth scrolling and
- // increase performance.
- if (index == mFocusPosition && subindex == mSubFocusPosition
- && mPendingMoveSmoothScroller == null) {
- dispatchChildSelected();
- }
- } else if (!mInFastRelayout) {
- // fastRelayout will dispatch event at end of onLayoutChildren().
- // For full layout, two situations here:
- // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
- // 2. mInLayoutSearchFocus is true: dispatchChildSelected() on first child
- // equal to or after mFocusPosition that can take focus.
- if (!mInLayoutSearchFocus && index == mFocusPosition
- && subindex == mSubFocusPosition) {
- dispatchChildSelected();
- } else if (mInLayoutSearchFocus && index >= mFocusPosition
- && v.hasFocusable()) {
- mFocusPosition = index;
- mSubFocusPosition = subindex;
- mInLayoutSearchFocus = false;
- dispatchChildSelected();
- }
- }
- measureChild(v);
- }
- item[0] = v;
- return mOrientation == HORIZONTAL ? getDecoratedMeasuredWidthWithMargin(v)
- : getDecoratedMeasuredHeightWithMargin(v);
- }
-
- @Override
- public void addItem(Object item, int index, int length, int rowIndex, int edge) {
- View v = (View) item;
- int start, end;
- if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
- edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingMin()
- : mWindowAlignment.mainAxis().getSize()
- - mWindowAlignment.mainAxis().getPaddingMax();
- }
- boolean edgeIsMin = !mGrid.isReversedFlow();
- if (edgeIsMin) {
- start = edge;
- end = edge + length;
- } else {
- start = edge - length;
- end = edge;
- }
- int startSecondary = getRowStartSecondary(rowIndex)
- + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
- mChildrenStates.loadView(v, index);
- layoutChild(rowIndex, v, start, end, startSecondary);
- if (DEBUG) {
- Log.d(getTag(), "addView " + index + " " + v);
- }
- if (TRACE) TraceCompat.endSection();
-
- if (!mState.isPreLayout()) {
- updateScrollLimits();
- }
- if (!mInLayout && mPendingMoveSmoothScroller != null) {
- mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
- }
- if (mChildLaidOutListener != null) {
- RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
- mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
- vh == null ? NO_ID : vh.getItemId());
- }
- }
-
- @Override
- public void removeItem(int index) {
- if (TRACE) TraceCompat.beginSection("removeItem");
- View v = findViewByPosition(index - mPositionDeltaInPreLayout);
- if (mInLayout) {
- detachAndScrapView(v, mRecycler);
- } else {
- removeAndRecycleView(v, mRecycler);
- }
- if (TRACE) TraceCompat.endSection();
- }
-
- @Override
- public int getEdge(int index) {
- View v = findViewByPosition(index - mPositionDeltaInPreLayout);
- return mReverseFlowPrimary ? getViewMax(v) : getViewMin(v);
- }
-
- @Override
- public int getSize(int index) {
- return getViewPrimarySize(findViewByPosition(index - mPositionDeltaInPreLayout));
- }
- };
-
- void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
- if (TRACE) TraceCompat.beginSection("layoutChild");
- int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v)
- : getDecoratedMeasuredWidthWithMargin(v);
- if (mFixedRowSizeSecondary > 0) {
- sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
- }
- final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary)
- ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK,
- View.LAYOUT_DIRECTION_RTL)
- : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
- if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP)
- || (mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT)) {
- // do nothing
- } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM)
- || (mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT)) {
- startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
- } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL)
- || (mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL)) {
- startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
- }
- int left, top, right, bottom;
- if (mOrientation == HORIZONTAL) {
- left = start;
- top = startSecondary;
- right = end;
- bottom = startSecondary + sizeSecondary;
- } else {
- top = start;
- left = startSecondary;
- bottom = end;
- right = startSecondary + sizeSecondary;
- }
- LayoutParams params = (LayoutParams) v.getLayoutParams();
- layoutDecoratedWithMargins(v, left, top, right, bottom);
- // Now super.getDecoratedBoundsWithMargins() includes the extra space for optical bounds,
- // subtracting it from value passed in layoutDecoratedWithMargins(), we can get the optical
- // bounds insets.
- super.getDecoratedBoundsWithMargins(v, sTempRect);
- params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top,
- sTempRect.right - right, sTempRect.bottom - bottom);
- updateChildAlignments(v);
- if (TRACE) TraceCompat.endSection();
- }
-
- private void updateChildAlignments(View v) {
- final LayoutParams p = (LayoutParams) v.getLayoutParams();
- if (p.getItemAlignmentFacet() == null) {
- // Fallback to global settings on grid view
- p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
- p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
- } else {
- // Use ItemAlignmentFacet defined on specific ViewHolder
- p.calculateItemAlignments(mOrientation, v);
- if (mOrientation == HORIZONTAL) {
- p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
- } else {
- p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
- }
- }
- }
-
- private void updateChildAlignments() {
- for (int i = 0, c = getChildCount(); i < c; i++) {
- updateChildAlignments(getChildAt(i));
- }
- }
-
- void setExtraLayoutSpace(int extraLayoutSpace) {
- if (mExtraLayoutSpace == extraLayoutSpace) {
- return;
- } else if (mExtraLayoutSpace < 0) {
- throw new IllegalArgumentException("ExtraLayoutSpace must >= 0");
- }
- mExtraLayoutSpace = extraLayoutSpace;
- requestLayout();
- }
-
- int getExtraLayoutSpace() {
- return mExtraLayoutSpace;
- }
-
- private void removeInvisibleViewsAtEnd() {
- if (mPruneChild && !mIsSlidingChildViews) {
- mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
- mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
- }
- }
-
- private void removeInvisibleViewsAtFront() {
- if (mPruneChild && !mIsSlidingChildViews) {
- mGrid.removeInvisibleItemsAtFront(mFocusPosition,
- mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace);
- }
- }
-
- private boolean appendOneColumnVisibleItems() {
- return mGrid.appendOneColumnVisibleItems();
- }
-
- void slideIn() {
- if (mIsSlidingChildViews) {
- mIsSlidingChildViews = false;
- if (mFocusPosition >= 0) {
- scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
- } else {
- mLayoutEatenInSliding = false;
- requestLayout();
- }
- if (mLayoutEatenInSliding) {
- mLayoutEatenInSliding = false;
- if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
- mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (newState == SCROLL_STATE_IDLE) {
- mBaseGridView.removeOnScrollListener(this);
- requestLayout();
- }
- }
- });
- } else {
- requestLayout();
- }
- }
- }
- }
-
- int getSlideOutDistance() {
- int distance;
- if (mOrientation == VERTICAL) {
- distance = -getHeight();
- if (getChildCount() > 0) {
- int top = getChildAt(0).getTop();
- if (top < 0) {
- // scroll more if first child is above top edge
- distance = distance + top;
- }
- }
- } else {
- if (mReverseFlowPrimary) {
- distance = getWidth();
- if (getChildCount() > 0) {
- int start = getChildAt(0).getRight();
- if (start > distance) {
- // scroll more if first child is outside right edge
- distance = start;
- }
- }
- } else {
- distance = -getWidth();
- if (getChildCount() > 0) {
- int start = getChildAt(0).getLeft();
- if (start < 0) {
- // scroll more if first child is out side left edge
- distance = distance + start;
- }
- }
- }
- }
- return distance;
- }
-
- /**
- * Temporarily slide out child and block layout and scroll requests.
- */
- void slideOut() {
- if (mIsSlidingChildViews) {
- return;
- }
- mIsSlidingChildViews = true;
- if (getChildCount() == 0) {
- return;
- }
- if (mOrientation == VERTICAL) {
- mBaseGridView.smoothScrollBy(0, getSlideOutDistance(),
- new AccelerateDecelerateInterpolator());
- } else {
- mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0,
- new AccelerateDecelerateInterpolator());
- }
- }
-
- private boolean prependOneColumnVisibleItems() {
- return mGrid.prependOneColumnVisibleItems();
- }
-
- private void appendVisibleItems() {
- mGrid.appendVisibleItems(mReverseFlowPrimary
- ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout
- : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout);
- }
-
- private void prependVisibleItems() {
- mGrid.prependVisibleItems(mReverseFlowPrimary
- ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout
- : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout);
- }
-
- /**
- * Fast layout when there is no structure change, adapter change, etc.
- * It will layout all views was layout requested or updated, until hit a view
- * with different size, then it break and detachAndScrap all views after that.
- */
- private void fastRelayout() {
- boolean invalidateAfter = false;
- final int childCount = getChildCount();
- int position = mGrid.getFirstVisibleIndex();
- int index = 0;
- for (; index < childCount; index++, position++) {
- View view = getChildAt(index);
- // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add
- // extra views and invalidate existing Grid position. Also the prelayout calling
- // getViewForPosotion() may retrieve item from cache with FLAG_INVALID. The adapter
- // postion will be -1 for this case. Either case, we should invalidate after this item
- // and call getViewForPosition() again to rebind.
- if (position != getAdapterPositionByView(view)) {
- invalidateAfter = true;
- break;
- }
- Grid.Location location = mGrid.getLocation(position);
- if (location == null) {
- invalidateAfter = true;
- break;
- }
-
- int startSecondary = getRowStartSecondary(location.row)
- + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
- int primarySize, end;
- int start = getViewMin(view);
- int oldPrimarySize = getViewPrimarySize(view);
-
- LayoutParams lp = (LayoutParams) view.getLayoutParams();
- if (lp.viewNeedsUpdate()) {
- detachAndScrapView(view, mRecycler);
- view = getViewForPosition(position);
- addView(view, index);
- }
-
- measureChild(view);
- if (mOrientation == HORIZONTAL) {
- primarySize = getDecoratedMeasuredWidthWithMargin(view);
- end = start + primarySize;
- } else {
- primarySize = getDecoratedMeasuredHeightWithMargin(view);
- end = start + primarySize;
- }
- layoutChild(location.row, view, start, end, startSecondary);
- if (oldPrimarySize != primarySize) {
- // size changed invalidate remaining Locations
- if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
- invalidateAfter = true;
- break;
- }
- }
- if (invalidateAfter) {
- final int savedLastPos = mGrid.getLastVisibleIndex();
- for (int i = childCount - 1; i >= index; i--) {
- View v = getChildAt(i);
- detachAndScrapView(v, mRecycler);
- }
- mGrid.invalidateItemsAfter(position);
- if (mPruneChild) {
- // in regular prune child mode, we just append items up to edge limit
- appendVisibleItems();
- if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
- // make sure add focus view back: the view might be outside edge limit
- // when there is delta in onLayoutChildren().
- while (mGrid.getLastVisibleIndex() < mFocusPosition) {
- mGrid.appendOneColumnVisibleItems();
- }
- }
- } else {
- // prune disabled(e.g. in RowsFragment transition): append all removed items
- while (mGrid.appendOneColumnVisibleItems()
- && mGrid.getLastVisibleIndex() < savedLastPos);
- }
- }
- updateScrollLimits();
- updateSecondaryScrollLimits();
- }
-
- @Override
- public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
- if (TRACE) TraceCompat.beginSection("removeAndRecycleAllViews");
- if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
- for (int i = getChildCount() - 1; i >= 0; i--) {
- removeAndRecycleViewAt(i, recycler);
- }
- if (TRACE) TraceCompat.endSection();
- }
-
- // called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable
- // and scroll to the view if framework focus on it.
- private void focusToViewInLayout(boolean hadFocus, boolean alignToView, int extraDelta,
- int extraDeltaSecondary) {
- View focusView = findViewByPosition(mFocusPosition);
- if (focusView != null && alignToView) {
- scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
- }
- if (focusView != null && hadFocus && !focusView.hasFocus()) {
- focusView.requestFocus();
- } else if (!hadFocus && !mBaseGridView.hasFocus()) {
- if (focusView != null && focusView.hasFocusable()) {
- mBaseGridView.focusableViewAvailable(focusView);
- } else {
- for (int i = 0, count = getChildCount(); i < count; i++) {
- focusView = getChildAt(i);
- if (focusView != null && focusView.hasFocusable()) {
- mBaseGridView.focusableViewAvailable(focusView);
- break;
- }
- }
- }
- // focusViewAvailable() might focus to the view, scroll to it if that is the case.
- if (alignToView && focusView != null && focusView.hasFocus()) {
- scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
- }
- }
- }
-
- @VisibleForTesting
- public static class OnLayoutCompleteListener {
- public void onLayoutCompleted(RecyclerView.State state) {
- }
- }
-
- @VisibleForTesting
- OnLayoutCompleteListener mLayoutCompleteListener;
-
- @Override
- public void onLayoutCompleted(State state) {
- if (mLayoutCompleteListener != null) {
- mLayoutCompleteListener.onLayoutCompleted(state);
- }
- }
-
- @Override
- public boolean supportsPredictiveItemAnimations() {
- return true;
- }
-
- void updatePositionToRowMapInPostLayout() {
- mPositionToRowInPostLayout.clear();
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- // Grid still maps to old positions at this point, use old position to get row infor
- int position = mBaseGridView.getChildViewHolder(getChildAt(i)).getOldPosition();
- if (position >= 0) {
- Grid.Location loc = mGrid.getLocation(position);
- if (loc != null) {
- mPositionToRowInPostLayout.put(position, loc.row);
- }
- }
- }
- }
-
- void fillScrapViewsInPostLayout() {
- List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList();
- final int scrapSize = scrapList.size();
- if (scrapSize == 0) {
- return;
- }
- // initialize the int array or re-allocate the array.
- if (mDisappearingPositions == null || scrapSize > mDisappearingPositions.length) {
- int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length;
- while (length < scrapSize) {
- length = length << 1;
- }
- mDisappearingPositions = new int[length];
- }
- int totalItems = 0;
- for (int i = 0; i < scrapSize; i++) {
- int pos = scrapList.get(i).getAdapterPosition();
- if (pos >= 0) {
- mDisappearingPositions[totalItems++] = pos;
- }
- }
- // totalItems now has the length of disappearing items
- if (totalItems > 0) {
- Arrays.sort(mDisappearingPositions, 0, totalItems);
- mGrid.fillDisappearingItems(mDisappearingPositions, totalItems,
- mPositionToRowInPostLayout);
- }
- mPositionToRowInPostLayout.clear();
- }
-
- // in prelayout, first child's getViewPosition can be smaller than old adapter position
- // if there were items removed before first visible index. For example:
- // visible items are 3, 4, 5, 6, deleting 1, 2, 3 from adapter; the view position in
- // prelayout are not 3(deleted), 4, 5, 6. Instead it's 1(deleted), 2, 3, 4.
- // So there is a delta (2 in this case) between last cached position and prelayout position.
- void updatePositionDeltaInPreLayout() {
- if (getChildCount() > 0) {
- View view = getChildAt(0);
- LayoutParams lp = (LayoutParams) view.getLayoutParams();
- mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex()
- - lp.getViewLayoutPosition();
- } else {
- mPositionDeltaInPreLayout = 0;
- }
- }
-
- // Lays out items based on the current scroll position
- @Override
- public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
- if (DEBUG) {
- Log.v(getTag(), "layoutChildren start numRows " + mNumRows
- + " inPreLayout " + state.isPreLayout()
- + " didStructureChange " + state.didStructureChange()
- + " mForceFullLayout " + mForceFullLayout);
- Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
- }
-
- if (mNumRows == 0) {
- // haven't done measure yet
- return;
- }
- final int itemCount = state.getItemCount();
- if (itemCount < 0) {
- return;
- }
-
- if (mIsSlidingChildViews) {
- // if there is already children, delay the layout process until slideIn(), if it's
- // first time layout children: scroll them offscreen at end of onLayoutChildren()
- if (getChildCount() > 0) {
- mLayoutEatenInSliding = true;
- return;
- }
- }
- if (!mLayoutEnabled) {
- discardLayoutInfo();
- removeAndRecycleAllViews(recycler);
- return;
- }
- mInLayout = true;
-
- saveContext(recycler, state);
- if (state.isPreLayout()) {
- updatePositionDeltaInPreLayout();
- int childCount = getChildCount();
- if (mGrid != null && childCount > 0) {
- int minChangedEdge = Integer.MAX_VALUE;
- int maxChangeEdge = Integer.MIN_VALUE;
- int minOldAdapterPosition = mBaseGridView.getChildViewHolder(
- getChildAt(0)).getOldPosition();
- int maxOldAdapterPosition = mBaseGridView.getChildViewHolder(
- getChildAt(childCount - 1)).getOldPosition();
- for (int i = 0; i < childCount; i++) {
- View view = getChildAt(i);
- LayoutParams lp = (LayoutParams) view.getLayoutParams();
- int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view);
- // if either of following happening
- // 1. item itself has changed or layout parameter changed
- // 2. item is losing focus
- // 3. item is gaining focus
- // 4. item is moved out of old adapter position range.
- if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested()
- || (!view.hasFocus() && mFocusPosition == lp.getViewAdapterPosition())
- || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition())
- || newAdapterPosition < minOldAdapterPosition
- || newAdapterPosition > maxOldAdapterPosition) {
- minChangedEdge = Math.min(minChangedEdge, getViewMin(view));
- maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view));
- }
- }
- if (maxChangeEdge > minChangedEdge) {
- mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge;
- }
- // append items for mExtraLayoutSpaceInPreLayout
- appendVisibleItems();
- prependVisibleItems();
- }
- mInLayout = false;
- leaveContext();
- if (DEBUG) Log.v(getTag(), "layoutChildren end");
- return;
- }
-
- // save all view's row information before detach all views
- if (state.willRunPredictiveAnimations()) {
- updatePositionToRowMapInPostLayout();
- }
- // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling
- final boolean scrollToFocus = !isSmoothScrolling()
- && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
- if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
- mFocusPosition = mFocusPosition + mFocusPositionOffset;
- mSubFocusPosition = 0;
- }
- mFocusPositionOffset = 0;
-
- View savedFocusView = findViewByPosition(mFocusPosition);
- int savedFocusPos = mFocusPosition;
- int savedSubFocusPos = mSubFocusPosition;
- boolean hadFocus = mBaseGridView.hasFocus();
- final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION;
- final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION;
- final int deltaPrimary;
- final int deltaSecondary;
- if (mOrientation == HORIZONTAL) {
- deltaPrimary = state.getRemainingScrollHorizontal();
- deltaSecondary = state.getRemainingScrollVertical();
- } else {
- deltaSecondary = state.getRemainingScrollHorizontal();
- deltaPrimary = state.getRemainingScrollVertical();
- }
- if (mInFastRelayout = layoutInit()) {
- // If grid view is empty, we will start from mFocusPosition
- mGrid.setStart(mFocusPosition);
- fastRelayout();
- } else {
- // layoutInit() has detached all views, so start from scratch
- mInLayoutSearchFocus = hadFocus;
- int startFromPosition, endPos;
- if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex
- || mFocusPosition < firstVisibleIndex)) {
- startFromPosition = endPos = mFocusPosition;
- } else {
- startFromPosition = firstVisibleIndex;
- endPos = lastVisibleIndex;
- }
- mGrid.setStart(startFromPosition);
- if (endPos != NO_POSITION) {
- while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) {
- // continuously append items until endPos
- }
- }
- }
- // multiple rounds: scrollToView of first round may drag first/last child into
- // "visible window" and we update scrollMin/scrollMax then run second scrollToView
- // we must do this for fastRelayout() for the append item case
- int oldFirstVisible;
- int oldLastVisible;
- do {
- updateScrollLimits();
- oldFirstVisible = mGrid.getFirstVisibleIndex();
- oldLastVisible = mGrid.getLastVisibleIndex();
- focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary);
- appendVisibleItems();
- prependVisibleItems();
- // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise
- // loop may bounce between scroll forward and scroll backward forever. Example:
- // Assuming there are 19 items, child#18 and child#19 are both in RV, we are
- // trying to focus to child#18 and there are 200px remaining scroll distance.
- // 1 focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on
- // right edge, but there to compensate remaining scroll 200px, also scroll
- // backward 200px, 150px pushes last child#19 out side of right edge.
- // 2 removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits()
- // invalidates scroll max
- // 3 In next iteration, when scroll max/min is unknown, focusToViewInLayout() will
- // align focused child#18 at center of screen.
- // 4 Because #18 is aligned at center, appendVisibleItems() will fill child#19 to
- // the right.
- // 5 (back to 1 and loop forever)
- } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
- || mGrid.getLastVisibleIndex() != oldLastVisible);
- removeInvisibleViewsAtFront();
- removeInvisibleViewsAtEnd();
-
- if (state.willRunPredictiveAnimations()) {
- fillScrapViewsInPostLayout();
- }
-
- if (DEBUG) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- mGrid.debugPrint(pw);
- Log.d(getTag(), sw.toString());
- }
-
- if (mRowSecondarySizeRefresh) {
- mRowSecondarySizeRefresh = false;
- } else {
- updateRowSecondarySizeRefresh();
- }
-
- // For fastRelayout, only dispatch event when focus position changes.
- if (mInFastRelayout && (mFocusPosition != savedFocusPos || mSubFocusPosition
- != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView)) {
- dispatchChildSelected();
- } else if (!mInFastRelayout && mInLayoutSearchFocus) {
- // For full layout we dispatchChildSelected() in createItem() unless searched all
- // children and found none is focusable then dispatchChildSelected() here.
- dispatchChildSelected();
- }
- dispatchChildSelectedAndPositioned();
- if (mIsSlidingChildViews) {
- scrollDirectionPrimary(getSlideOutDistance());
- }
-
- mInLayout = false;
- leaveContext();
- if (DEBUG) Log.v(getTag(), "layoutChildren end");
- }
-
- private void offsetChildrenSecondary(int increment) {
- final int childCount = getChildCount();
- if (mOrientation == HORIZONTAL) {
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetTopAndBottom(increment);
- }
- } else {
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetLeftAndRight(increment);
- }
- }
- }
-
- private void offsetChildrenPrimary(int increment) {
- final int childCount = getChildCount();
- if (mOrientation == VERTICAL) {
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetTopAndBottom(increment);
- }
- } else {
- for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetLeftAndRight(increment);
- }
- }
- }
-
- @Override
- public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
- if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
- if (!mLayoutEnabled || !hasDoneFirstLayout()) {
- return 0;
- }
- saveContext(recycler, state);
- mInScroll = true;
- int result;
- if (mOrientation == HORIZONTAL) {
- result = scrollDirectionPrimary(dx);
- } else {
- result = scrollDirectionSecondary(dx);
- }
- leaveContext();
- mInScroll = false;
- return result;
- }
-
- @Override
- public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
- if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
- if (!mLayoutEnabled || !hasDoneFirstLayout()) {
- return 0;
- }
- mInScroll = true;
- saveContext(recycler, state);
- int result;
- if (mOrientation == VERTICAL) {
- result = scrollDirectionPrimary(dy);
- } else {
- result = scrollDirectionSecondary(dy);
- }
- leaveContext();
- mInScroll = false;
- return result;
- }
-
- // scroll in main direction may add/prune views
- private int scrollDirectionPrimary(int da) {
- if (TRACE) TraceCompat.beginSection("scrollPrimary");
- // We apply the cap of maxScroll/minScroll to the delta, except for two cases:
- // 1. when children are in sliding out mode
- // 2. During onLayoutChildren(), it may compensate the remaining scroll delta,
- // we should honor the request regardless if it goes over minScroll / maxScroll.
- // (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1)
- if (!mIsSlidingChildViews && !mInLayout) {
- if (da > 0) {
- if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
- int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
- if (da > maxScroll) {
- da = maxScroll;
- }
- }
- } else if (da < 0) {
- if (!mWindowAlignment.mainAxis().isMinUnknown()) {
- int minScroll = mWindowAlignment.mainAxis().getMinScroll();
- if (da < minScroll) {
- da = minScroll;
- }
- }
- }
- }
- if (da == 0) {
- if (TRACE) TraceCompat.endSection();
- return 0;
- }
- offsetChildrenPrimary(-da);
- if (mInLayout) {
- updateScrollLimits();
- if (TRACE) TraceCompat.endSection();
- return da;
- }
-
- int childCount = getChildCount();
- boolean updated;
-
- if (mReverseFlowPrimary ? da > 0 : da < 0) {
- prependVisibleItems();
- } else {
- appendVisibleItems();
- }
- updated = getChildCount() > childCount;
- childCount = getChildCount();
-
- if (TRACE) TraceCompat.beginSection("remove");
- if (mReverseFlowPrimary ? da > 0 : da < 0) {
- removeInvisibleViewsAtEnd();
- } else {
- removeInvisibleViewsAtFront();
- }
- if (TRACE) TraceCompat.endSection();
- updated |= getChildCount() < childCount;
- if (updated) {
- updateRowSecondarySizeRefresh();
- }
-
- mBaseGridView.invalidate();
- updateScrollLimits();
- if (TRACE) TraceCompat.endSection();
- return da;
- }
-
- // scroll in second direction will not add/prune views
- private int scrollDirectionSecondary(int dy) {
- if (dy == 0) {
- return 0;
- }
- offsetChildrenSecondary(-dy);
- mScrollOffsetSecondary += dy;
- updateSecondaryScrollLimits();
- mBaseGridView.invalidate();
- return dy;
- }
-
- @Override
- public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
- LayoutPrefetchRegistry layoutPrefetchRegistry) {
- try {
- saveContext(null, state);
- int da = (mOrientation == HORIZONTAL) ? dx : dy;
- if (getChildCount() == 0 || da == 0) {
- // can't support this scroll, so don't bother prefetching
- return;
- }
-
- int fromLimit = da < 0
- ? -mExtraLayoutSpace
- : mSizePrimary + mExtraLayoutSpace;
- mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry);
- } finally {
- leaveContext();
- }
- }
-
- @Override
- public void collectInitialPrefetchPositions(int adapterItemCount,
- LayoutPrefetchRegistry layoutPrefetchRegistry) {
- int numToPrefetch = mBaseGridView.mInitialPrefetchItemCount;
- if (adapterItemCount != 0 && numToPrefetch != 0) {
- // prefetch items centered around mFocusPosition
- int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1)/ 2,
- adapterItemCount - numToPrefetch));
- for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) {
- layoutPrefetchRegistry.addPosition(i, 0);
- }
- }
- }
-
- void updateScrollLimits() {
- if (mState.getItemCount() == 0) {
- return;
- }
- int highVisiblePos, lowVisiblePos;
- int highMaxPos, lowMinPos;
- if (!mReverseFlowPrimary) {
- highVisiblePos = mGrid.getLastVisibleIndex();
- highMaxPos = mState.getItemCount() - 1;
- lowVisiblePos = mGrid.getFirstVisibleIndex();
- lowMinPos = 0;
- } else {
- highVisiblePos = mGrid.getFirstVisibleIndex();
- highMaxPos = 0;
- lowVisiblePos = mGrid.getLastVisibleIndex();
- lowMinPos = mState.getItemCount() - 1;
- }
- if (highVisiblePos < 0 || lowVisiblePos < 0) {
- return;
- }
- final boolean highAvailable = highVisiblePos == highMaxPos;
- final boolean lowAvailable = lowVisiblePos == lowMinPos;
- if (!highAvailable && mWindowAlignment.mainAxis().isMaxUnknown()
- && !lowAvailable && mWindowAlignment.mainAxis().isMinUnknown()) {
- return;
- }
- int maxEdge, maxViewCenter;
- if (highAvailable) {
- maxEdge = mGrid.findRowMax(true, sTwoInts);
- View maxChild = findViewByPosition(sTwoInts[1]);
- maxViewCenter = getViewCenter(maxChild);
- final LayoutParams lp = (LayoutParams) maxChild.getLayoutParams();
- int[] multipleAligns = lp.getAlignMultiple();
- if (multipleAligns != null && multipleAligns.length > 0) {
- maxViewCenter += multipleAligns[multipleAligns.length - 1] - multipleAligns[0];
- }
- } else {
- maxEdge = Integer.MAX_VALUE;
- maxViewCenter = Integer.MAX_VALUE;
- }
- int minEdge, minViewCenter;
- if (lowAvailable) {
- minEdge = mGrid.findRowMin(false, sTwoInts);
- View minChild = findViewByPosition(sTwoInts[1]);
- minViewCenter = getViewCenter(minChild);
- } else {
- minEdge = Integer.MIN_VALUE;
- minViewCenter = Integer.MIN_VALUE;
- }
- mWindowAlignment.mainAxis().updateMinMax(minEdge, maxEdge, minViewCenter, maxViewCenter);
- }
-
- /**
- * Update secondary axis's scroll min/max, should be updated in
- * {@link #scrollDirectionSecondary(int)}.
- */
- private void updateSecondaryScrollLimits() {
- WindowAlignment.Axis secondAxis = mWindowAlignment.secondAxis();
- int minEdge = secondAxis.getPaddingMin() - mScrollOffsetSecondary;
- int maxEdge = minEdge + getSizeSecondary();
- secondAxis.updateMinMax(minEdge, maxEdge, minEdge, maxEdge);
- }
-
- private void initScrollController() {
- mWindowAlignment.reset();
- mWindowAlignment.horizontal.setSize(getWidth());
- mWindowAlignment.vertical.setSize(getHeight());
- mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
- mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
- mSizePrimary = mWindowAlignment.mainAxis().getSize();
- mScrollOffsetSecondary = 0;
-
- if (DEBUG) {
- Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
- + " mWindowAlignment " + mWindowAlignment);
- }
- }
-
- private void updateScrollController() {
- mWindowAlignment.horizontal.setSize(getWidth());
- mWindowAlignment.vertical.setSize(getHeight());
- mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
- mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
- mSizePrimary = mWindowAlignment.mainAxis().getSize();
-
- if (DEBUG) {
- Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
- + " mWindowAlignment " + mWindowAlignment);
- }
- }
-
- @Override
- public void scrollToPosition(int position) {
- setSelection(position, 0, false, 0);
- }
-
- @Override
- public void smoothScrollToPosition(RecyclerView recyclerView, State state,
- int position) {
- setSelection(position, 0, true, 0);
- }
-
- public void setSelection(int position,
- int primaryScrollExtra) {
- setSelection(position, 0, false, primaryScrollExtra);
- }
-
- public void setSelectionSmooth(int position) {
- setSelection(position, 0, true, 0);
- }
-
- public void setSelectionWithSub(int position, int subposition,
- int primaryScrollExtra) {
- setSelection(position, subposition, false, primaryScrollExtra);
- }
-
- public void setSelectionSmoothWithSub(int position, int subposition) {
- setSelection(position, subposition, true, 0);
- }
-
- public int getSelection() {
- return mFocusPosition;
- }
-
- public int getSubSelection() {
- return mSubFocusPosition;
- }
-
- public void setSelection(int position, int subposition, boolean smooth,
- int primaryScrollExtra) {
- if ((mFocusPosition != position && position != NO_POSITION)
- || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
- scrollToSelection(position, subposition, smooth, primaryScrollExtra);
- }
- }
-
- void scrollToSelection(int position, int subposition,
- boolean smooth, int primaryScrollExtra) {
- if (TRACE) TraceCompat.beginSection("scrollToSelection");
- mPrimaryScrollExtra = primaryScrollExtra;
- View view = findViewByPosition(position);
- // scrollToView() is based on Adapter position. Only call scrollToView() when item
- // is still valid.
- if (view != null && getAdapterPositionByView(view) == position) {
- mInSelection = true;
- scrollToView(view, smooth);
- mInSelection = false;
- } else {
- mFocusPosition = position;
- mSubFocusPosition = subposition;
- mFocusPositionOffset = Integer.MIN_VALUE;
- if (!mLayoutEnabled || mIsSlidingChildViews) {
- return;
- }
- if (smooth) {
- if (!hasDoneFirstLayout()) {
- Log.w(getTag(), "setSelectionSmooth should "
- + "not be called before first layout pass");
- return;
- }
- position = startPositionSmoothScroller(position);
- if (position != mFocusPosition) {
- // gets cropped by adapter size
- mFocusPosition = position;
- mSubFocusPosition = 0;
- }
- } else {
- mForceFullLayout = true;
- requestLayout();
- }
- }
- if (TRACE) TraceCompat.endSection();
- }
-
- int startPositionSmoothScroller(int position) {
- LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() {
- @Override
- public PointF computeScrollVectorForPosition(int targetPosition) {
- if (getChildCount() == 0) {
- return null;
- }
- final int firstChildPos = getPosition(getChildAt(0));
- // TODO We should be able to deduce direction from bounds of current and target
- // focus, rather than making assumptions about positions and directionality
- final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos
- : targetPosition < firstChildPos;
- final int direction = isStart ? -1 : 1;
- if (mOrientation == HORIZONTAL) {
- return new PointF(direction, 0);
- } else {
- return new PointF(0, direction);
- }
- }
-
- };
- linearSmoothScroller.setTargetPosition(position);
- startSmoothScroll(linearSmoothScroller);
- return linearSmoothScroller.getTargetPosition();
- }
-
- private void processPendingMovement(boolean forward) {
- if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
- return;
- }
- if (mPendingMoveSmoothScroller == null) {
- // Stop existing scroller and create a new PendingMoveSmoothScroller.
- mBaseGridView.stopScroll();
- PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
- forward ? 1 : -1, mNumRows > 1);
- mFocusPositionOffset = 0;
- startSmoothScroll(linearSmoothScroller);
- if (linearSmoothScroller.isRunning()) {
- mPendingMoveSmoothScroller = linearSmoothScroller;
- }
- } else {
- if (forward) {
- mPendingMoveSmoothScroller.increasePendingMoves();
- } else {
- mPendingMoveSmoothScroller.decreasePendingMoves();
- }
- }
- }
-
- @Override
- public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
- if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
- + positionStart + " itemCount " + itemCount);
- if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
- && mFocusPositionOffset != Integer.MIN_VALUE) {
- int pos = mFocusPosition + mFocusPositionOffset;
- if (positionStart <= pos) {
- mFocusPositionOffset += itemCount;
- }
- }
- mChildrenStates.clear();
- }
-
- @Override
- public void onItemsChanged(RecyclerView recyclerView) {
- if (DEBUG) Log.v(getTag(), "onItemsChanged");
- mFocusPositionOffset = 0;
- mChildrenStates.clear();
- }
-
- @Override
- public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
- if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
- + positionStart + " itemCount " + itemCount);
- if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
- && mFocusPositionOffset != Integer.MIN_VALUE) {
- int pos = mFocusPosition + mFocusPositionOffset;
- if (positionStart <= pos) {
- if (positionStart + itemCount > pos) {
- // stop updating offset after the focus item was removed
- mFocusPositionOffset += positionStart - pos;
- mFocusPosition += mFocusPositionOffset;
- mFocusPositionOffset = Integer.MIN_VALUE;
- } else {
- mFocusPositionOffset -= itemCount;
- }
- }
- }
- mChildrenStates.clear();
- }
-
- @Override
- public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
- int itemCount) {
- if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
- + fromPosition + " toPosition " + toPosition);
- if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
- int pos = mFocusPosition + mFocusPositionOffset;
- if (fromPosition <= pos && pos < fromPosition + itemCount) {
- // moved items include focused position
- mFocusPositionOffset += toPosition - fromPosition;
- } else if (fromPosition < pos && toPosition > pos - itemCount) {
- // move items before focus position to after focused position
- mFocusPositionOffset -= itemCount;
- } else if (fromPosition > pos && toPosition < pos) {
- // move items after focus position to before focused position
- mFocusPositionOffset += itemCount;
- }
- }
- mChildrenStates.clear();
- }
-
- @Override
- public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
- if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
- + positionStart + " itemCount " + itemCount);
- for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
- mChildrenStates.remove(i);
- }
- }
-
- @Override
- public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
- if (mFocusSearchDisabled) {
- return true;
- }
- if (getAdapterPositionByView(child) == NO_POSITION) {
- // This is could be the last view in DISAPPEARING animation.
- return true;
- }
- if (!mInLayout && !mInSelection && !mInScroll) {
- scrollToView(child, focused, true);
- }
- return true;
- }
-
- @Override
- public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
- boolean immediate) {
- if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
- return false;
- }
-
- public void getViewSelectedOffsets(View view, int[] offsets) {
- if (mOrientation == HORIZONTAL) {
- offsets[0] = getPrimaryAlignedScrollDistance(view);
- offsets[1] = getSecondaryScrollDistance(view);
- } else {
- offsets[1] = getPrimaryAlignedScrollDistance(view);
- offsets[0] = getSecondaryScrollDistance(view);
- }
- }
-
- /**
- * Return the scroll delta on primary direction to make the view selected. If the return value
- * is 0, there is no need to scroll.
- */
- private int getPrimaryAlignedScrollDistance(View view) {
- return mWindowAlignment.mainAxis().getScroll(getViewCenter(view));
- }
-
- /**
- * Get adjusted primary position for a given childView (if there is multiple ItemAlignment
- * defined on the view).
- */
- private int getAdjustedPrimaryAlignedScrollDistance(int scrollPrimary, View view,
- View childView) {
- int subindex = getSubPositionByView(view, childView);
- if (subindex != 0) {
- final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0];
- }
- return scrollPrimary;
- }
-
- private int getSecondaryScrollDistance(View view) {
- int viewCenterSecondary = getViewCenterSecondary(view);
- return mWindowAlignment.secondAxis().getScroll(viewCenterSecondary);
- }
-
- /**
- * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
- */
- void scrollToView(View view, boolean smooth) {
- scrollToView(view, view == null ? null : view.findFocus(), smooth);
- }
-
- void scrollToView(View view, boolean smooth, int extraDelta, int extraDeltaSecondary) {
- scrollToView(view, view == null ? null : view.findFocus(), smooth, extraDelta,
- extraDeltaSecondary);
- }
-
- private void scrollToView(View view, View childView, boolean smooth) {
- scrollToView(view, childView, smooth, 0, 0);
- }
- /**
- * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
- */
- private void scrollToView(View view, View childView, boolean smooth, int extraDelta,
- int extraDeltaSecondary) {
- if (mIsSlidingChildViews) {
- return;
- }
- int newFocusPosition = getAdapterPositionByView(view);
- int newSubFocusPosition = getSubPositionByView(view, childView);
- if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
- mFocusPosition = newFocusPosition;
- mSubFocusPosition = newSubFocusPosition;
- mFocusPositionOffset = 0;
- if (!mInLayout) {
- dispatchChildSelected();
- }
- if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
- mBaseGridView.invalidate();
- }
- }
- if (view == null) {
- return;
- }
- if (!view.hasFocus() && mBaseGridView.hasFocus()) {
- // transfer focus to the child if it does not have focus yet (e.g. triggered
- // by setSelection())
- view.requestFocus();
- }
- if (!mScrollEnabled && smooth) {
- return;
- }
- if (getScrollPosition(view, childView, sTwoInts)
- || extraDelta != 0 || extraDeltaSecondary != 0) {
- scrollGrid(sTwoInts[0] + extraDelta, sTwoInts[1] + extraDeltaSecondary, smooth);
- }
- }
-
- boolean getScrollPosition(View view, View childView, int[] deltas) {
- switch (mFocusScrollStrategy) {
- case BaseGridView.FOCUS_SCROLL_ALIGNED:
- default:
- return getAlignedPosition(view, childView, deltas);
- case BaseGridView.FOCUS_SCROLL_ITEM:
- case BaseGridView.FOCUS_SCROLL_PAGE:
- return getNoneAlignedPosition(view, deltas);
- }
- }
-
- private boolean getNoneAlignedPosition(View view, int[] deltas) {
- int pos = getAdapterPositionByView(view);
- int viewMin = getViewMin(view);
- int viewMax = getViewMax(view);
- // we either align "firstView" to left/top padding edge
- // or align "lastView" to right/bottom padding edge
- View firstView = null;
- View lastView = null;
- int paddingMin = mWindowAlignment.mainAxis().getPaddingMin();
- int clientSize = mWindowAlignment.mainAxis().getClientSize();
- final int row = mGrid.getRowIndex(pos);
- if (viewMin < paddingMin) {
- // view enters low padding area:
- firstView = view;
- if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
- // scroll one "page" left/top,
- // align first visible item of the "page" at the low padding edge.
- while (prependOneColumnVisibleItems()) {
- CircularIntArray positions =
- mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
- firstView = findViewByPosition(positions.get(0));
- if (viewMax - getViewMin(firstView) > clientSize) {
- if (positions.size() > 2) {
- firstView = findViewByPosition(positions.get(2));
- }
- break;
- }
- }
- }
- } else if (viewMax > clientSize + paddingMin) {
- // view enters high padding area:
- if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
- // scroll whole one page right/bottom, align view at the low padding edge.
- firstView = view;
- do {
- CircularIntArray positions =
- mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
- lastView = findViewByPosition(positions.get(positions.size() - 1));
- if (getViewMax(lastView) - viewMin > clientSize) {
- lastView = null;
- break;
- }
- } while (appendOneColumnVisibleItems());
- if (lastView != null) {
- // however if we reached end, we should align last view.
- firstView = null;
- }
- } else {
- lastView = view;
- }
- }
- int scrollPrimary = 0;
- int scrollSecondary = 0;
- if (firstView != null) {
- scrollPrimary = getViewMin(firstView) - paddingMin;
- } else if (lastView != null) {
- scrollPrimary = getViewMax(lastView) - (paddingMin + clientSize);
- }
- View secondaryAlignedView;
- if (firstView != null) {
- secondaryAlignedView = firstView;
- } else if (lastView != null) {
- secondaryAlignedView = lastView;
- } else {
- secondaryAlignedView = view;
- }
- scrollSecondary = getSecondaryScrollDistance(secondaryAlignedView);
- if (scrollPrimary != 0 || scrollSecondary != 0) {
- deltas[0] = scrollPrimary;
- deltas[1] = scrollSecondary;
- return true;
- }
- return false;
- }
-
- private boolean getAlignedPosition(View view, View childView, int[] deltas) {
- int scrollPrimary = getPrimaryAlignedScrollDistance(view);
- if (childView != null) {
- scrollPrimary = getAdjustedPrimaryAlignedScrollDistance(scrollPrimary, view, childView);
- }
- int scrollSecondary = getSecondaryScrollDistance(view);
- if (DEBUG) {
- Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
- + " " + mPrimaryScrollExtra + " " + mWindowAlignment);
- }
- scrollPrimary += mPrimaryScrollExtra;
- if (scrollPrimary != 0 || scrollSecondary != 0) {
- deltas[0] = scrollPrimary;
- deltas[1] = scrollSecondary;
- return true;
- } else {
- deltas[0] = 0;
- deltas[1] = 0;
- }
- return false;
- }
-
- private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
- if (mInLayout) {
- scrollDirectionPrimary(scrollPrimary);
- scrollDirectionSecondary(scrollSecondary);
- } else {
- int scrollX;
- int scrollY;
- if (mOrientation == HORIZONTAL) {
- scrollX = scrollPrimary;
- scrollY = scrollSecondary;
- } else {
- scrollX = scrollSecondary;
- scrollY = scrollPrimary;
- }
- if (smooth) {
- mBaseGridView.smoothScrollBy(scrollX, scrollY);
- } else {
- mBaseGridView.scrollBy(scrollX, scrollY);
- dispatchChildSelectedAndPositioned();
- }
- }
- }
-
- public void setPruneChild(boolean pruneChild) {
- if (mPruneChild != pruneChild) {
- mPruneChild = pruneChild;
- if (mPruneChild) {
- requestLayout();
- }
- }
- }
-
- public boolean getPruneChild() {
- return mPruneChild;
- }
-
- public void setScrollEnabled(boolean scrollEnabled) {
- if (mScrollEnabled != scrollEnabled) {
- mScrollEnabled = scrollEnabled;
- if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
- && mFocusPosition != NO_POSITION) {
- scrollToSelection(mFocusPosition, mSubFocusPosition,
- true, mPrimaryScrollExtra);
- }
- }
- }
-
- public boolean isScrollEnabled() {
- return mScrollEnabled;
- }
-
- private int findImmediateChildIndex(View view) {
- if (mBaseGridView != null && view != mBaseGridView) {
- view = findContainingItemView(view);
- if (view != null) {
- for (int i = 0, count = getChildCount(); i < count; i++) {
- if (getChildAt(i) == view) {
- return i;
- }
- }
- }
- }
- return NO_POSITION;
- }
-
- void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
- if (gainFocus) {
- // if gridview.requestFocus() is called, select first focusable child.
- for (int i = mFocusPosition; ;i++) {
- View view = findViewByPosition(i);
- if (view == null) {
- break;
- }
- if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) {
- view.requestFocus();
- break;
- }
- }
- }
- }
-
- void setFocusSearchDisabled(boolean disabled) {
- mFocusSearchDisabled = disabled;
- }
-
- boolean isFocusSearchDisabled() {
- return mFocusSearchDisabled;
- }
-
- @Override
- public View onInterceptFocusSearch(View focused, int direction) {
- if (mFocusSearchDisabled) {
- return focused;
- }
-
- final FocusFinder ff = FocusFinder.getInstance();
- View result = null;
- if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
- // convert direction to absolute direction and see if we have a view there and if not
- // tell LayoutManager to add if it can.
- if (canScrollVertically()) {
- final int absDir =
- direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
- result = ff.findNextFocus(mBaseGridView, focused, absDir);
- }
- if (canScrollHorizontally()) {
- boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
- final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
- ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
- result = ff.findNextFocus(mBaseGridView, focused, absDir);
- }
- } else {
- result = ff.findNextFocus(mBaseGridView, focused, direction);
- }
- if (result != null) {
- return result;
- }
-
- if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
- return mBaseGridView.getParent().focusSearch(focused, direction);
- }
-
- if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction);
- int movement = getMovement(direction);
- final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
- if (movement == NEXT_ITEM) {
- if (isScroll || !mFocusOutEnd) {
- result = focused;
- }
- if (mScrollEnabled && !hasCreatedLastItem()) {
- processPendingMovement(true);
- result = focused;
- }
- } else if (movement == PREV_ITEM) {
- if (isScroll || !mFocusOutFront) {
- result = focused;
- }
- if (mScrollEnabled && !hasCreatedFirstItem()) {
- processPendingMovement(false);
- result = focused;
- }
- } else if (movement == NEXT_ROW) {
- if (isScroll || !mFocusOutSideEnd) {
- result = focused;
- }
- } else if (movement == PREV_ROW) {
- if (isScroll || !mFocusOutSideStart) {
- result = focused;
- }
- }
- if (result != null) {
- return result;
- }
-
- if (DEBUG) Log.v(getTag(), "now focusSearch in parent");
- result = mBaseGridView.getParent().focusSearch(focused, direction);
- if (result != null) {
- return result;
- }
- return focused != null ? focused : mBaseGridView;
- }
-
- boolean hasPreviousViewInSameRow(int pos) {
- if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
- return false;
- }
- if (mGrid.getFirstVisibleIndex() > 0) {
- return true;
- }
- final int focusedRow = mGrid.getLocation(pos).row;
- for (int i = getChildCount() - 1; i >= 0; i--) {
- int position = getAdapterPositionByIndex(i);
- Grid.Location loc = mGrid.getLocation(position);
- if (loc != null && loc.row == focusedRow) {
- if (position < pos) {
- return true;
- }
- }
- }
- return false;
- }
-
- @Override
- public boolean onAddFocusables(RecyclerView recyclerView,
- ArrayList<View> views, int direction, int focusableMode) {
- if (mFocusSearchDisabled) {
- return true;
- }
- // If this viewgroup or one of its children currently has focus then we
- // consider our children for focus searching in main direction on the same row.
- // If this viewgroup has no focus and using focus align, we want the system
- // to ignore our children and pass focus to the viewgroup, which will pass
- // focus on to its children appropriately.
- // If this viewgroup has no focus and not using focus align, we want to
- // consider the child that does not overlap with padding area.
- if (recyclerView.hasFocus()) {
- if (mPendingMoveSmoothScroller != null) {
- // don't find next focusable if has pending movement.
- return true;
- }
- final int movement = getMovement(direction);
- final View focused = recyclerView.findFocus();
- final int focusedIndex = findImmediateChildIndex(focused);
- final int focusedPos = getAdapterPositionByIndex(focusedIndex);
- // Even if focusedPos != NO_POSITION, findViewByPosition could return null if the view
- // is ignored or getLayoutPosition does not match the adapter position of focused view.
- final View immediateFocusedChild = (focusedPos == NO_POSITION) ? null
- : findViewByPosition(focusedPos);
- // Add focusables of focused item.
- if (immediateFocusedChild != null) {
- immediateFocusedChild.addFocusables(views, direction, focusableMode);
- }
- if (mGrid == null || getChildCount() == 0) {
- // no grid information, or no child, bail out.
- return true;
- }
- if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) {
- // For single row, cannot navigate to previous/next row.
- return true;
- }
- // Add focusables of neighbor depending on the focus search direction.
- final int focusedRow = mGrid != null && immediateFocusedChild != null
- ? mGrid.getLocation(focusedPos).row : NO_POSITION;
- final int focusableCount = views.size();
- int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1;
- int loop_end = inc > 0 ? getChildCount() - 1 : 0;
- int loop_start;
- if (focusedIndex == NO_POSITION) {
- loop_start = inc > 0 ? 0 : getChildCount() - 1;
- } else {
- loop_start = focusedIndex + inc;
- }
- for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) {
- final View child = getChildAt(i);
- if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) {
- continue;
- }
- // if there wasn't any focused item, add the very first focusable
- // items and stop.
- if (immediateFocusedChild == null) {
- child.addFocusables(views, direction, focusableMode);
- if (views.size() > focusableCount) {
- break;
- }
- continue;
- }
- int position = getAdapterPositionByIndex(i);
- Grid.Location loc = mGrid.getLocation(position);
- if (loc == null) {
- continue;
- }
- if (movement == NEXT_ITEM) {
- // Add first focusable item on the same row
- if (loc.row == focusedRow && position > focusedPos) {
- child.addFocusables(views, direction, focusableMode);
- if (views.size() > focusableCount) {
- break;
- }
- }
- } else if (movement == PREV_ITEM) {
- // Add first focusable item on the same row
- if (loc.row == focusedRow && position < focusedPos) {
- child.addFocusables(views, direction, focusableMode);
- if (views.size() > focusableCount) {
- break;
- }
- }
- } else if (movement == NEXT_ROW) {
- // Add all focusable items after this item whose row index is bigger
- if (loc.row == focusedRow) {
- continue;
- } else if (loc.row < focusedRow) {
- break;
- }
- child.addFocusables(views, direction, focusableMode);
- } else if (movement == PREV_ROW) {
- // Add all focusable items before this item whose row index is smaller
- if (loc.row == focusedRow) {
- continue;
- } else if (loc.row > focusedRow) {
- break;
- }
- child.addFocusables(views, direction, focusableMode);
- }
- }
- } else {
- int focusableCount = views.size();
- if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
- // adding views not overlapping padding area to avoid scrolling in gaining focus
- int left = mWindowAlignment.mainAxis().getPaddingMin();
- int right = mWindowAlignment.mainAxis().getClientSize() + left;
- for (int i = 0, count = getChildCount(); i < count; i++) {
- View child = getChildAt(i);
- if (child.getVisibility() == View.VISIBLE) {
- if (getViewMin(child) >= left && getViewMax(child) <= right) {
- child.addFocusables(views, direction, focusableMode);
- }
- }
- }
- // if we cannot find any, then just add all children.
- if (views.size() == focusableCount) {
- for (int i = 0, count = getChildCount(); i < count; i++) {
- View child = getChildAt(i);
- if (child.getVisibility() == View.VISIBLE) {
- child.addFocusables(views, direction, focusableMode);
- }
- }
- }
- } else {
- View view = findViewByPosition(mFocusPosition);
- if (view != null) {
- view.addFocusables(views, direction, focusableMode);
- }
- }
- // if still cannot find any, fall through and add itself
- if (views.size() != focusableCount) {
- return true;
- }
- if (recyclerView.isFocusable()) {
- views.add(recyclerView);
- }
- }
- return true;
- }
-
- boolean hasCreatedLastItem() {
- int count = getItemCount();
- return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null;
- }
-
- boolean hasCreatedFirstItem() {
- int count = getItemCount();
- return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null;
- }
-
- boolean isItemFullyVisible(int pos) {
- RecyclerView.ViewHolder vh = mBaseGridView.findViewHolderForAdapterPosition(pos);
- if (vh == null) {
- return false;
- }
- return vh.itemView.getLeft() >= 0 && vh.itemView.getRight() < mBaseGridView.getWidth()
- && vh.itemView.getTop() >= 0 && vh.itemView.getBottom() < mBaseGridView.getHeight();
- }
-
- boolean canScrollTo(View view) {
- return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
- }
-
- boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
- Rect previouslyFocusedRect) {
- switch (mFocusScrollStrategy) {
- case BaseGridView.FOCUS_SCROLL_ALIGNED:
- default:
- return gridOnRequestFocusInDescendantsAligned(recyclerView,
- direction, previouslyFocusedRect);
- case BaseGridView.FOCUS_SCROLL_PAGE:
- case BaseGridView.FOCUS_SCROLL_ITEM:
- return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
- direction, previouslyFocusedRect);
- }
- }
-
- private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
- int direction, Rect previouslyFocusedRect) {
- View view = findViewByPosition(mFocusPosition);
- if (view != null) {
- boolean result = view.requestFocus(direction, previouslyFocusedRect);
- if (!result && DEBUG) {
- Log.w(getTag(), "failed to request focus on " + view);
- }
- return result;
- }
- return false;
- }
-
- private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
- int direction, Rect previouslyFocusedRect) {
- // focus to view not overlapping padding area to avoid scrolling in gaining focus
- int index;
- int increment;
- int end;
- int count = getChildCount();
- if ((direction & View.FOCUS_FORWARD) != 0) {
- index = 0;
- increment = 1;
- end = count;
- } else {
- index = count - 1;
- increment = -1;
- end = -1;
- }
- int left = mWindowAlignment.mainAxis().getPaddingMin();
- int right = mWindowAlignment.mainAxis().getClientSize() + left;
- for (int i = index; i != end; i += increment) {
- View child = getChildAt(i);
- if (child.getVisibility() == View.VISIBLE) {
- if (getViewMin(child) >= left && getViewMax(child) <= right) {
- if (child.requestFocus(direction, previouslyFocusedRect)) {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- private final static int PREV_ITEM = 0;
- private final static int NEXT_ITEM = 1;
- private final static int PREV_ROW = 2;
- private final static int NEXT_ROW = 3;
-
- private int getMovement(int direction) {
- int movement = View.FOCUS_LEFT;
-
- if (mOrientation == HORIZONTAL) {
- switch(direction) {
- case View.FOCUS_LEFT:
- movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
- break;
- case View.FOCUS_RIGHT:
- movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
- break;
- case View.FOCUS_UP:
- movement = PREV_ROW;
- break;
- case View.FOCUS_DOWN:
- movement = NEXT_ROW;
- break;
- }
- } else if (mOrientation == VERTICAL) {
- switch(direction) {
- case View.FOCUS_LEFT:
- movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW;
- break;
- case View.FOCUS_RIGHT:
- movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW;
- break;
- case View.FOCUS_UP:
- movement = PREV_ITEM;
- break;
- case View.FOCUS_DOWN:
- movement = NEXT_ITEM;
- break;
- }
- }
-
- return movement;
- }
-
- int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
- View view = findViewByPosition(mFocusPosition);
- if (view == null) {
- return i;
- }
- int focusIndex = recyclerView.indexOfChild(view);
- // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
- // drawing order is 0 1 2 3 9 8 7 6 5 4
- if (i < focusIndex) {
- return i;
- } else if (i < childCount - 1) {
- return focusIndex + childCount - 1 - i;
- } else {
- return focusIndex;
- }
- }
-
- @Override
- public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
- RecyclerView.Adapter newAdapter) {
- if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
- if (oldAdapter != null) {
- discardLayoutInfo();
- mFocusPosition = NO_POSITION;
- mFocusPositionOffset = 0;
- mChildrenStates.clear();
- }
- if (newAdapter instanceof FacetProviderAdapter) {
- mFacetProviderAdapter = (FacetProviderAdapter) newAdapter;
- } else {
- mFacetProviderAdapter = null;
- }
- super.onAdapterChanged(oldAdapter, newAdapter);
- }
-
- private void discardLayoutInfo() {
- mGrid = null;
- mRowSizeSecondary = null;
- mRowSecondarySizeRefresh = false;
- }
-
- public void setLayoutEnabled(boolean layoutEnabled) {
- if (mLayoutEnabled != layoutEnabled) {
- mLayoutEnabled = layoutEnabled;
- requestLayout();
- }
- }
-
- void setChildrenVisibility(int visibility) {
- mChildVisibility = visibility;
- if (mChildVisibility != -1) {
- int count = getChildCount();
- for (int i= 0; i < count; i++) {
- getChildAt(i).setVisibility(mChildVisibility);
- }
- }
- }
-
- final static class SavedState implements Parcelable {
-
- int index; // index inside adapter of the current view
- Bundle childStates = Bundle.EMPTY;
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(index);
- out.writeBundle(childStates);
- }
-
- @SuppressWarnings("hiding")
- public static final Parcelable.Creator<SavedState> CREATOR =
- new Parcelable.Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- SavedState(Parcel in) {
- index = in.readInt();
- childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
- }
-
- SavedState() {
- }
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
- SavedState ss = new SavedState();
- // save selected index
- ss.index = getSelection();
- // save offscreen child (state when they are recycled)
- Bundle bundle = mChildrenStates.saveAsBundle();
- // save views currently is on screen (TODO save cached views)
- for (int i = 0, count = getChildCount(); i < count; i++) {
- View view = getChildAt(i);
- int position = getAdapterPositionByView(view);
- if (position != NO_POSITION) {
- bundle = mChildrenStates.saveOnScreenView(bundle, view, position);
- }
- }
- ss.childStates = bundle;
- return ss;
- }
-
- void onChildRecycled(RecyclerView.ViewHolder holder) {
- final int position = holder.getAdapterPosition();
- if (position != NO_POSITION) {
- mChildrenStates.saveOffscreenView(holder.itemView, position);
- }
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- return;
- }
- SavedState loadingState = (SavedState)state;
- mFocusPosition = loadingState.index;
- mFocusPositionOffset = 0;
- mChildrenStates.loadFromBundle(loadingState.childStates);
- mForceFullLayout = true;
- requestLayout();
- if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
- }
-
- @Override
- public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
- RecyclerView.State state) {
- if (mOrientation == HORIZONTAL && mGrid != null) {
- return mGrid.getNumRows();
- }
- return super.getRowCountForAccessibility(recycler, state);
- }
-
- @Override
- public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
- RecyclerView.State state) {
- if (mOrientation == VERTICAL && mGrid != null) {
- return mGrid.getNumRows();
- }
- return super.getColumnCountForAccessibility(recycler, state);
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
- RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
- ViewGroup.LayoutParams lp = host.getLayoutParams();
- if (mGrid == null || !(lp instanceof LayoutParams)) {
- return;
- }
- LayoutParams glp = (LayoutParams) lp;
- int position = glp.getViewAdapterPosition();
- int rowIndex = position >= 0 ? mGrid.getRowIndex(position) : -1;
- if (rowIndex < 0) {
- return;
- }
- int guessSpanIndex = position / mGrid.getNumRows();
- if (mOrientation == HORIZONTAL) {
- info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
- rowIndex, 1, guessSpanIndex, 1, false, false));
- } else {
- info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
- guessSpanIndex, 1, rowIndex, 1, false, false));
- }
- }
-
- /*
- * Leanback widget is different than the default implementation because the "scroll" is driven
- * by selection change.
- */
- @Override
- public boolean performAccessibilityAction(Recycler recycler, State state, int action,
- Bundle args) {
- saveContext(recycler, state);
- switch (action) {
- case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
- processSelectionMoves(false, -1);
- break;
- case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
- processSelectionMoves(false, 1);
- break;
- }
- leaveContext();
- return true;
- }
-
- /*
- * Move mFocusPosition multiple steps on the same row in main direction.
- * Stops when moves are all consumed or reach first/last visible item.
- * Returning remaining moves.
- */
- int processSelectionMoves(boolean preventScroll, int moves) {
- if (mGrid == null) {
- return moves;
- }
- int focusPosition = mFocusPosition;
- int focusedRow = focusPosition != NO_POSITION
- ? mGrid.getRowIndex(focusPosition) : NO_POSITION;
- View newSelected = null;
- for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) {
- int index = moves > 0 ? i : count - 1 - i;
- final View child = getChildAt(index);
- if (!canScrollTo(child)) {
- continue;
- }
- int position = getAdapterPositionByIndex(index);
- int rowIndex = mGrid.getRowIndex(position);
- if (focusedRow == NO_POSITION) {
- focusPosition = position;
- newSelected = child;
- focusedRow = rowIndex;
- } else if (rowIndex == focusedRow) {
- if ((moves > 0 && position > focusPosition)
- || (moves < 0 && position < focusPosition)) {
- focusPosition = position;
- newSelected = child;
- if (moves > 0) {
- moves--;
- } else {
- moves++;
- }
- }
- }
- }
- if (newSelected != null) {
- if (preventScroll) {
- if (hasFocus()) {
- mInSelection = true;
- newSelected.requestFocus();
- mInSelection = false;
- }
- mFocusPosition = focusPosition;
- mSubFocusPosition = 0;
- } else {
- scrollToView(newSelected, true);
- }
- }
- return moves;
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
- AccessibilityNodeInfoCompat info) {
- saveContext(recycler, state);
- int count = state.getItemCount();
- if (mScrollEnabled && count > 1 && !isItemFullyVisible(0)) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
- info.setScrollable(true);
- }
- if (mScrollEnabled && count > 1 && !isItemFullyVisible(count - 1)) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
- info.setScrollable(true);
- }
- final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
- AccessibilityNodeInfoCompat.CollectionInfoCompat
- .obtain(getRowCountForAccessibility(recycler, state),
- getColumnCountForAccessibility(recycler, state),
- isLayoutHierarchical(recycler, state),
- getSelectionModeForAccessibility(recycler, state));
- info.setCollectionInfo(collectionInfo);
- leaveContext();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
deleted file mode 100644
index 5b755f5..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
+++ /dev/null
@@ -1,496 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ViewHolder;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.inputmethod.EditorInfo;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.TextView.OnEditorActionListener;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
- * Presentation (view creation and state animation) is delegated to a {@link
- * GuidedActionsStylist}, while clients are notified of interactions via
- * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class GuidedActionAdapter extends RecyclerView.Adapter {
- static final String TAG = "GuidedActionAdapter";
- static final boolean DEBUG = false;
-
- static final String TAG_EDIT = "EditableAction";
- static final boolean DEBUG_EDIT = false;
-
- /**
- * Object listening for click events within a {@link GuidedActionAdapter}.
- */
- public interface ClickListener {
-
- /**
- * Called when the user clicks on an action.
- */
- void onGuidedActionClicked(GuidedAction action);
-
- }
-
- /**
- * Object listening for focus events within a {@link GuidedActionAdapter}.
- */
- public interface FocusListener {
-
- /**
- * Called when the user focuses on an action.
- */
- void onGuidedActionFocused(GuidedAction action);
- }
-
- /**
- * Object listening for edit events within a {@link GuidedActionAdapter}.
- */
- public interface EditListener {
-
- /**
- * Called when the user exits edit mode on an action.
- */
- void onGuidedActionEditCanceled(GuidedAction action);
-
- /**
- * Called when the user exits edit mode on an action and process confirm button in IME.
- */
- long onGuidedActionEditedAndProceed(GuidedAction action);
-
- /**
- * Called when Ime Open
- */
- void onImeOpen();
-
- /**
- * Called when Ime Close
- */
- void onImeClose();
- }
-
- private final boolean mIsSubAdapter;
- private final ActionOnKeyListener mActionOnKeyListener;
- private final ActionOnFocusListener mActionOnFocusListener;
- private final ActionEditListener mActionEditListener;
- private final List<GuidedAction> mActions;
- private ClickListener mClickListener;
- final GuidedActionsStylist mStylist;
- GuidedActionAdapterGroup mGroup;
-
- private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (v != null && v.getWindowToken() != null && getRecyclerView() != null) {
- GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
- getRecyclerView().getChildViewHolder(v);
- GuidedAction action = avh.getAction();
- if (action.hasTextEditable()) {
- if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
- mGroup.openIme(GuidedActionAdapter.this, avh);
- } else if (action.hasEditableActivatorView()) {
- if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
- performOnActionClick(avh);
- } else {
- handleCheckedActions(avh);
- if (action.isEnabled() && !action.infoOnly()) {
- performOnActionClick(avh);
- }
- }
- }
- }
- };
-
- /**
- * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
- * focus listeners, and the given presenter.
- * @param actions The list of guided actions this adapter will manage.
- * @param focusListener The focus listener for items in this adapter.
- * @param presenter The presenter that will manage the display of items in this adapter.
- */
- public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
- FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) {
- super();
- mActions = actions == null ? new ArrayList<GuidedAction>() :
- new ArrayList<GuidedAction>(actions);
- mClickListener = clickListener;
- mStylist = presenter;
- mActionOnKeyListener = new ActionOnKeyListener();
- mActionOnFocusListener = new ActionOnFocusListener(focusListener);
- mActionEditListener = new ActionEditListener();
- mIsSubAdapter = isSubAdapter;
- }
-
- /**
- * Sets the list of actions managed by this adapter.
- * @param actions The list of actions to be managed.
- */
- public void setActions(List<GuidedAction> actions) {
- if (!mIsSubAdapter) {
- mStylist.collapseAction(false);
- }
- mActionOnFocusListener.unFocus();
- mActions.clear();
- mActions.addAll(actions);
- notifyDataSetChanged();
- }
-
- /**
- * Returns the count of actions managed by this adapter.
- * @return The count of actions managed by this adapter.
- */
- public int getCount() {
- return mActions.size();
- }
-
- /**
- * Returns the GuidedAction at the given position in the managed list.
- * @param position The position of the desired GuidedAction.
- * @return The GuidedAction at the given position.
- */
- public GuidedAction getItem(int position) {
- return mActions.get(position);
- }
-
- /**
- * Return index of action in array
- * @param action Action to search index.
- * @return Index of Action in array.
- */
- public int indexOf(GuidedAction action) {
- return mActions.indexOf(action);
- }
-
- /**
- * @return GuidedActionsStylist used to build the actions list UI.
- */
- public GuidedActionsStylist getGuidedActionsStylist() {
- return mStylist;
- }
-
- /**
- * Sets the click listener for items managed by this adapter.
- * @param clickListener The click listener for this adapter.
- */
- public void setClickListener(ClickListener clickListener) {
- mClickListener = clickListener;
- }
-
- /**
- * Sets the focus listener for items managed by this adapter.
- * @param focusListener The focus listener for this adapter.
- */
- public void setFocusListener(FocusListener focusListener) {
- mActionOnFocusListener.setFocusListener(focusListener);
- }
-
- /**
- * Used for serialization only.
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public List<GuidedAction> getActions() {
- return new ArrayList<GuidedAction>(mActions);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int getItemViewType(int position) {
- return mStylist.getItemViewType(mActions.get(position));
- }
-
- RecyclerView getRecyclerView() {
- return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType);
- View v = vh.itemView;
- v.setOnKeyListener(mActionOnKeyListener);
- v.setOnClickListener(mOnClickListener);
- v.setOnFocusChangeListener(mActionOnFocusListener);
-
- setupListeners(vh.getEditableTitleView());
- setupListeners(vh.getEditableDescriptionView());
-
- return vh;
- }
-
- private void setupListeners(EditText edit) {
- if (edit != null) {
- edit.setPrivateImeOptions("EscapeNorth=1;");
- edit.setOnEditorActionListener(mActionEditListener);
- if (edit instanceof ImeKeyMonitor) {
- ImeKeyMonitor monitor = (ImeKeyMonitor)edit;
- monitor.setImeKeyListener(mActionEditListener);
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- if (position >= mActions.size()) {
- return;
- }
- final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder;
- GuidedAction action = mActions.get(position);
- mStylist.onBindViewHolder(avh, action);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public int getItemCount() {
- return mActions.size();
- }
-
- private class ActionOnFocusListener implements View.OnFocusChangeListener {
-
- private FocusListener mFocusListener;
- private View mSelectedView;
-
- ActionOnFocusListener(FocusListener focusListener) {
- mFocusListener = focusListener;
- }
-
- public void setFocusListener(FocusListener focusListener) {
- mFocusListener = focusListener;
- }
-
- public void unFocus() {
- if (mSelectedView != null && getRecyclerView() != null) {
- ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView);
- if (vh != null) {
- GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh;
- mStylist.onAnimateItemFocused(avh, false);
- } else {
- Log.w(TAG, "RecyclerView returned null view holder",
- new Throwable());
- }
- }
- }
-
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (getRecyclerView() == null) {
- return;
- }
- GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
- getRecyclerView().getChildViewHolder(v);
- if (hasFocus) {
- mSelectedView = v;
- if (mFocusListener != null) {
- // We still call onGuidedActionFocused so that listeners can clear
- // state if they want.
- mFocusListener.onGuidedActionFocused(avh.getAction());
- }
- } else {
- if (mSelectedView == v) {
- mStylist.onAnimateItemPressedCancelled(avh);
- mSelectedView = null;
- }
- }
- mStylist.onAnimateItemFocused(avh, hasFocus);
- }
- }
-
- public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) {
- // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy
- if (getRecyclerView() == null) {
- return null;
- }
- GuidedActionsStylist.ViewHolder result = null;
- ViewParent parent = v.getParent();
- while (parent != getRecyclerView() && parent != null && v != null) {
- v = (View)parent;
- parent = parent.getParent();
- }
- if (parent != null && v != null) {
- result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v);
- }
- return result;
- }
-
- public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) {
- GuidedAction action = avh.getAction();
- int actionCheckSetId = action.getCheckSetId();
- if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) {
- // Find any actions that are checked and are in the same group
- // as the selected action. Fade their checkmarks out.
- if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) {
- for (int i = 0, size = mActions.size(); i < size; i++) {
- GuidedAction a = mActions.get(i);
- if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
- a.setChecked(false);
- GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder)
- getRecyclerView().findViewHolderForPosition(i);
- if (vh != null) {
- mStylist.onAnimateItemChecked(vh, false);
- }
- }
- }
- }
-
- // If we we'ren't already checked, fade our checkmark in.
- if (!action.isChecked()) {
- action.setChecked(true);
- mStylist.onAnimateItemChecked(avh, true);
- } else {
- if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) {
- action.setChecked(false);
- mStylist.onAnimateItemChecked(avh, false);
- }
- }
- }
- }
-
- public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) {
- if (mClickListener != null) {
- mClickListener.onGuidedActionClicked(avh.getAction());
- }
- }
-
- private class ActionOnKeyListener implements View.OnKeyListener {
-
- private boolean mKeyPressed = false;
-
- ActionOnKeyListener() {
- }
-
- /**
- * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
- */
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (v == null || event == null || getRecyclerView() == null) {
- return false;
- }
- boolean handled = false;
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_NUMPAD_ENTER:
- case KeyEvent.KEYCODE_BUTTON_X:
- case KeyEvent.KEYCODE_BUTTON_Y:
- case KeyEvent.KEYCODE_ENTER:
-
- GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
- getRecyclerView().getChildViewHolder(v);
- GuidedAction action = avh.getAction();
-
- if (!action.isEnabled() || action.infoOnly()) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- // TODO: requires API 19
- //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
- }
- return true;
- }
-
- switch (event.getAction()) {
- case KeyEvent.ACTION_DOWN:
- if (DEBUG) {
- Log.d(TAG, "Enter Key down");
- }
- if (!mKeyPressed) {
- mKeyPressed = true;
- mStylist.onAnimateItemPressed(avh, mKeyPressed);
- }
- break;
- case KeyEvent.ACTION_UP:
- if (DEBUG) {
- Log.d(TAG, "Enter Key up");
- }
- // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
- // Escape in IME.
- if (mKeyPressed) {
- mKeyPressed = false;
- mStylist.onAnimateItemPressed(avh, mKeyPressed);
- }
- break;
- default:
- break;
- }
- break;
- default:
- break;
- }
- return handled;
- }
-
- }
-
- private class ActionEditListener implements OnEditorActionListener,
- ImeKeyMonitor.ImeKeyListener {
-
- ActionEditListener() {
- }
-
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
- boolean handled = false;
- if (actionId == EditorInfo.IME_ACTION_NEXT
- || actionId == EditorInfo.IME_ACTION_DONE) {
- mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
- handled = true;
- } else if (actionId == EditorInfo.IME_ACTION_NONE) {
- if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
- // Escape north handling: stay on current item, but close editor
- handled = true;
- mGroup.fillAndStay(GuidedActionAdapter.this, v);
- }
- return handled;
- }
-
- @Override
- public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
- if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
- if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
- mGroup.fillAndStay(GuidedActionAdapter.this, editText);
- return true;
- } else if (keyCode == KeyEvent.KEYCODE_ENTER
- && event.getAction() == KeyEvent.ACTION_UP) {
- mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
- return true;
- }
- return false;
- }
-
- }
-
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
deleted file mode 100644
index 535f81b..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.widget;
-
-import android.database.Observable;
-
-/**
- * Base class adapter to be used in leanback activities. Provides access to a data model and is
- * decoupled from the presentation of the items via {@link PresenterSelector}.
- */
-public abstract class ObjectAdapter {
-
- /** Indicates that an id has not been set. */
- public static final int NO_ID = -1;
-
- /**
- * A DataObserver can be notified when an ObjectAdapter's underlying data
- * changes. Separate methods provide notifications about different types of
- * changes.
- */
- public static abstract class DataObserver {
- /**
- * Called whenever the ObjectAdapter's data has changed in some manner
- * outside of the set of changes covered by the other range-based change
- * notification methods.
- */
- public void onChanged() {
- }
-
- /**
- * Called when a range of items in the ObjectAdapter has changed. The
- * basic ordering and structure of the ObjectAdapter has not changed.
- *
- * @param positionStart The position of the first item that changed.
- * @param itemCount The number of items changed.
- */
- public void onItemRangeChanged(int positionStart, int itemCount) {
- onChanged();
- }
-
- /**
- * Called when a range of items in the ObjectAdapter has changed. The
- * basic ordering and structure of the ObjectAdapter has not changed.
- *
- * @param positionStart The position of the first item that changed.
- * @param itemCount The number of items changed.
- * @param payload Optional parameter, use null to identify a "full" update.
- */
- public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
- onChanged();
- }
-
- /**
- * Called when a range of items is inserted into the ObjectAdapter.
- *
- * @param positionStart The position of the first inserted item.
- * @param itemCount The number of items inserted.
- */
- public void onItemRangeInserted(int positionStart, int itemCount) {
- onChanged();
- }
-
- /**
- * Called when an item is moved from one position to another position
- *
- * @param fromPosition Previous position of the item.
- * @param toPosition New position of the item.
- */
- public void onItemMoved(int fromPosition, int toPosition) {
- onChanged();
- }
-
- /**
- * Called when a range of items is removed from the ObjectAdapter.
- *
- * @param positionStart The position of the first removed item.
- * @param itemCount The number of items removed.
- */
- public void onItemRangeRemoved(int positionStart, int itemCount) {
- onChanged();
- }
- }
-
- private static final class DataObservable extends Observable<DataObserver> {
-
- DataObservable() {
- }
-
- public void notifyChanged() {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onChanged();
- }
- }
-
- public void notifyItemRangeChanged(int positionStart, int itemCount) {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
- }
- }
-
- public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
- }
- }
-
- public void notifyItemRangeInserted(int positionStart, int itemCount) {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
- }
- }
-
- public void notifyItemRangeRemoved(int positionStart, int itemCount) {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
- }
- }
-
- public void notifyItemMoved(int positionStart, int toPosition) {
- for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemMoved(positionStart, toPosition);
- }
- }
- }
-
- private final DataObservable mObservable = new DataObservable();
- private boolean mHasStableIds;
- private PresenterSelector mPresenterSelector;
-
- /**
- * Constructs an adapter with the given {@link PresenterSelector}.
- */
- public ObjectAdapter(PresenterSelector presenterSelector) {
- setPresenterSelector(presenterSelector);
- }
-
- /**
- * Constructs an adapter that uses the given {@link Presenter} for all items.
- */
- public ObjectAdapter(Presenter presenter) {
- setPresenterSelector(new SinglePresenterSelector(presenter));
- }
-
- /**
- * Constructs an adapter.
- */
- public ObjectAdapter() {
- }
-
- /**
- * Sets the presenter selector. May not be null.
- */
- public final void setPresenterSelector(PresenterSelector presenterSelector) {
- if (presenterSelector == null) {
- throw new IllegalArgumentException("Presenter selector must not be null");
- }
- final boolean update = (mPresenterSelector != null);
- final boolean selectorChanged = update && mPresenterSelector != presenterSelector;
-
- mPresenterSelector = presenterSelector;
-
- if (selectorChanged) {
- onPresenterSelectorChanged();
- }
- if (update) {
- notifyChanged();
- }
- }
-
- /**
- * Called when {@link #setPresenterSelector(PresenterSelector)} is called
- * and the PresenterSelector differs from the previous one.
- */
- protected void onPresenterSelectorChanged() {
- }
-
- /**
- * Returns the presenter selector for this ObjectAdapter.
- */
- public final PresenterSelector getPresenterSelector() {
- return mPresenterSelector;
- }
-
- /**
- * Registers a DataObserver for data change notifications.
- */
- public final void registerObserver(DataObserver observer) {
- mObservable.registerObserver(observer);
- }
-
- /**
- * Unregisters a DataObserver for data change notifications.
- */
- public final void unregisterObserver(DataObserver observer) {
- mObservable.unregisterObserver(observer);
- }
-
- /**
- * Unregisters all DataObservers for this ObjectAdapter.
- */
- public final void unregisterAllObservers() {
- mObservable.unregisterAll();
- }
-
- /**
- * Notifies UI that some items has changed.
- *
- * @param positionStart Starting position of the changed items.
- * @param itemCount Total number of items that changed.
- */
- public final void notifyItemRangeChanged(int positionStart, int itemCount) {
- mObservable.notifyItemRangeChanged(positionStart, itemCount);
- }
-
- /**
- * Notifies UI that some items has changed.
- *
- * @param positionStart Starting position of the changed items.
- * @param itemCount Total number of items that changed.
- * @param payload Optional parameter, use null to identify a "full" update.
- */
- public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
- mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
- }
-
- /**
- * Notifies UI that new items has been inserted.
- *
- * @param positionStart Position where new items has been inserted.
- * @param itemCount Count of the new items has been inserted.
- */
- final protected void notifyItemRangeInserted(int positionStart, int itemCount) {
- mObservable.notifyItemRangeInserted(positionStart, itemCount);
- }
-
- /**
- * Notifies UI that some items that has been removed.
- *
- * @param positionStart Starting position of the removed items.
- * @param itemCount Total number of items that has been removed.
- */
- final protected void notifyItemRangeRemoved(int positionStart, int itemCount) {
- mObservable.notifyItemRangeRemoved(positionStart, itemCount);
- }
-
- /**
- * Notifies UI that item at fromPosition has been moved to toPosition.
- *
- * @param fromPosition Previous position of the item.
- * @param toPosition New position of the item.
- */
- protected final void notifyItemMoved(int fromPosition, int toPosition) {
- mObservable.notifyItemMoved(fromPosition, toPosition);
- }
-
- /**
- * Notifies UI that the underlying data has changed.
- */
- final protected void notifyChanged() {
- mObservable.notifyChanged();
- }
-
- /**
- * Returns true if the item ids are stable across changes to the
- * underlying data. When this is true, clients of the ObjectAdapter can use
- * {@link #getId(int)} to correlate Objects across changes.
- */
- public final boolean hasStableIds() {
- return mHasStableIds;
- }
-
- /**
- * Sets whether the item ids are stable across changes to the underlying
- * data.
- */
- public final void setHasStableIds(boolean hasStableIds) {
- boolean changed = mHasStableIds != hasStableIds;
- mHasStableIds = hasStableIds;
-
- if (changed) {
- onHasStableIdsChanged();
- }
- }
-
- /**
- * Called when {@link #setHasStableIds(boolean)} is called and the status
- * of stable ids has changed.
- */
- protected void onHasStableIdsChanged() {
- }
-
- /**
- * Returns the {@link Presenter} for the given item from the adapter.
- */
- public final Presenter getPresenter(Object item) {
- if (mPresenterSelector == null) {
- throw new IllegalStateException("Presenter selector must not be null");
- }
- return mPresenterSelector.getPresenter(item);
- }
-
- /**
- * Returns the number of items in the adapter.
- */
- public abstract int size();
-
- /**
- * Returns the item for the given position.
- */
- public abstract Object get(int position);
-
- /**
- * Returns the id for the given position.
- */
- public long getId(int position) {
- return NO_ID;
- }
-
- /**
- * Returns true if the adapter pairs each underlying data change with a call to notify and
- * false otherwise.
- */
- public boolean isImmediateNotifySupported() {
- return false;
- }
-}
diff --git a/v17/leanback/tests/Android.mk b/v17/leanback/tests/Android.mk
deleted file mode 100644
index 6c1a709..0000000
--- a/v17/leanback/tests/Android.mk
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_RESOURCE_DIR = \
- $(LOCAL_PATH)/res \
- $(LOCAL_PATH)/../res \
- $(LOCAL_PATH)/../../v7/recyclerview/res
-LOCAL_AAPT_FLAGS := \
- --auto-add-overlay \
- --extra-packages android.support.v17.leanback \
- --extra-packages android.support.v7.recyclerview
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-v4 \
- android-support-v7-recyclerview \
- android-support-v17-leanback \
- android-support-test \
- mockito-target-minus-junit4
-
-LOCAL_PACKAGE_NAME := AndroidLeanbackTests
-
-include $(BUILD_PACKAGE)
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
deleted file mode 100755
index d87ff6f..0000000
--- a/v17/leanback/tests/generatev4.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/python
-
-# 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.
-
-import os
-import sys
-
-print "Generate v4 fragment related code for leanback"
-
-####### generate XXXTestFragment classes #######
-
-files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest', 'DetailsTest']
-
-cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
- 'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
- 'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video',
- 'DetailsTest']
-
-for w in files:
- print "copy {}Fragment to {}SupportFragment".format(w, w)
-
- file = open('java/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'r')
- outfile = open('java/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'w')
-
- outfile.write("// CHECKSTYLE:OFF Generated code\n")
- outfile.write("/* This file is auto-generated from {}Fragment.java. DO NOT MODIFY. */\n\n".format(w))
-
- for line in file:
- for w in cls:
- line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
- line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
- line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
- line = line.replace('Activity getActivity()', 'FragmentActivity getActivity()')
- outfile.write(line)
- file.close()
- outfile.close()
-
-####### generate XXXFragmentTestBase classes #######
-
-testcls = ['GuidedStep', 'Single']
-
-for w in testcls:
- print "copy {}FrgamentTestBase to {}SupportFragmentTestBase".format(w, w)
-
- file = open('java/android/support/v17/leanback/app/{}FragmentTestBase.java'.format(w), 'r')
- outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTestBase.java'.format(w), 'w')
-
- outfile.write("// CHECKSTYLE:OFF Generated code\n")
- outfile.write("/* This file is auto-generated from {}FrgamentTestBase.java. DO NOT MODIFY. */\n\n".format(w))
-
- for line in file:
- for w in cls:
- line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
- for w in testcls:
- line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
- line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
- line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
- line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
- line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
- outfile.write(line)
- file.close()
- outfile.close()
-
-####### generate XXXFragmentTest classes #######
-
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
-
-for w in testcls:
- print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
-
- file = open('java/android/support/v17/leanback/app/{}FragmentTest.java'.format(w), 'r')
- outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTest.java'.format(w), 'w')
-
- outfile.write("// CHECKSTYLE:OFF Generated code\n")
- outfile.write("/* This file is auto-generated from {}FragmentTest.java. DO NOT MODIFY. */\n\n".format(w))
-
- for line in file:
- for w in cls:
- line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
- for w in testcls:
- line = line.replace('SingleFragmentTestBase', 'SingleSupportFragmentTestBase')
- line = line.replace('SingleFragmentTestActivity', 'SingleSupportFragmentTestActivity')
- line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
- line = line.replace('{}FragmentTest'.format(w), '{}SupportFragmentTest'.format(w))
- line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
- line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
- line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
- line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
- line = line.replace('extends Activity', 'extends FragmentActivity')
- line = line.replace('Activity.this.getFragmentManager', 'Activity.this.getSupportFragmentManager')
- line = line.replace('tivity.getFragmentManager', 'tivity.getSupportFragmentManager')
- outfile.write(line)
- file.close()
- outfile.close()
-
-
-####### generate XXXTestActivity classes #######
-testcls = ['Browse', 'GuidedStep', 'Single']
-
-for w in testcls:
- print "copy {}FragmentTestActivity to {}SupportFragmentTestActivity".format(w, w)
- file = open('java/android/support/v17/leanback/app/{}FragmentTestActivity.java'.format(w), 'r')
- outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTestActivity.java'.format(w), 'w')
- outfile.write("// CHECKSTYLE:OFF Generated code\n")
- outfile.write("/* This file is auto-generated from {}FragmentTestActivity.java. DO NOT MODIFY. */\n\n".format(w))
- for line in file:
- line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
- line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
- line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
- line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
- line = line.replace('extends Activity', 'extends FragmentActivity')
- line = line.replace('getFragmentManager', 'getSupportFragmentManager')
- outfile.write(line)
- file.close()
- outfile.close()
-
-####### generate Float parallax test #######
-
-print "copy ParallaxIntEffectTest to ParallaxFloatEffectTest"
-file = open('java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java', 'r')
-outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java', 'w')
-outfile.write("// CHECKSTYLE:OFF Generated code\n")
-outfile.write("/* This file is auto-generated from ParallaxIntEffectTest.java. DO NOT MODIFY. */\n\n")
-for line in file:
- line = line.replace('IntEffect', 'FloatEffect')
- line = line.replace('IntParallax', 'FloatParallax')
- line = line.replace('IntProperty', 'FloatProperty')
- line = line.replace('intValue()', 'floatValue()')
- line = line.replace('int screenMax', 'float screenMax')
- line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
- line = line.replace('(int)', '(float)')
- line = line.replace('int[', 'float[')
- line = line.replace('Integer', 'Float');
- outfile.write(line)
-file.close()
-outfile.close()
-
-
-print "copy ParallaxIntTest to ParallaxFloatTest"
-file = open('java/android/support/v17/leanback/widget/ParallaxIntTest.java', 'r')
-outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatTest.java', 'w')
-outfile.write("// CHECKSTYLE:OFF Generated code\n")
-outfile.write("/* This file is auto-generated from ParallaxIntTest.java. DO NOT MODIFY. */\n\n")
-for line in file:
- line = line.replace('ParallaxIntTest', 'ParallaxFloatTest')
- line = line.replace('IntParallax', 'FloatParallax')
- line = line.replace('IntProperty', 'FloatProperty')
- line = line.replace('verifyIntProperties', 'verifyFloatProperties')
- line = line.replace('intValue()', 'floatValue()')
- line = line.replace('int screenMax', 'float screenMax')
- line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
- line = line.replace('(int)', '(float)')
- outfile.write(line)
-file.close()
-outfile.close()
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
deleted file mode 100644
index 06a1217..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
-import android.content.Intent;
-import android.os.Build;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v7.widget.RecyclerView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class BrowseFragmentTest {
-
- static final String TAG = "BrowseFragmentTest";
- static final long WAIT_TRANSIITON_TIMEOUT = 10000;
-
- @Rule
- public ActivityTestRule<BrowseFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(BrowseFragmentTestActivity.class, false, false);
- private BrowseFragmentTestActivity mActivity;
-
- @After
- public void afterTest() throws Throwable {
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (mActivity != null) {
- mActivity.finish();
- mActivity = null;
- }
- }
- });
- }
-
- void waitForEntranceTransitionFinished() {
- PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- if (Build.VERSION.SDK_INT >= 21) {
- return mActivity.getBrowseTestFragment() != null
- && mActivity.getBrowseTestFragment().mEntranceTransitionEnded;
- } else {
- // when entrance transition not supported, wait main fragment loaded.
- return mActivity.getBrowseTestFragment() != null
- && mActivity.getBrowseTestFragment().getMainFragment() != null;
- }
- }
- });
- }
-
- void waitForHeaderTransitionFinished() {
- View row = mActivity.getBrowseTestFragment().getRowsFragment().getRowViewHolder(
- mActivity.getBrowseTestFragment().getSelectedPosition()).view;
- PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.ViewStableOnScreen(row));
- }
-
- @Test
- public void testTwoBackKeysWithBackStack() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- assertNotNull(mActivity.getBrowseTestFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- waitForHeaderTransitionFinished();
- sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
- }
-
- @Test
- public void testTwoBackKeysWithoutBackStack() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- assertNotNull(mActivity.getBrowseTestFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- waitForHeaderTransitionFinished();
- sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
- }
-
- @Test
- public void testPressRightBeforeMainFragmentCreated() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- mActivity = activityTestRule.launchActivity(intent);
-
- assertNull(mActivity.getBrowseTestFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- }
-
- @Test
- public void testSelectCardOnARow() throws Throwable {
- final int selectRow = 10;
- final int selectItem = 20;
- Intent intent = new Intent();
- final long dataLoadingDelay = 1000;
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- Presenter.ViewHolderTask itemTask = Mockito.spy(
- new ItemSelectionTask(mActivity, selectRow));
-
- final ListRowPresenter.SelectItemViewHolderTask task =
- new ListRowPresenter.SelectItemViewHolderTask(selectItem);
- task.setItemTask(itemTask);
-
- mActivity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.getBrowseTestFragment().setSelectedPosition(selectRow, true, task);
- }
- });
-
- verify(itemTask, timeout(5000).times(1)).run(any(Presenter.ViewHolder.class));
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ListRowPresenter.ViewHolder row = (ListRowPresenter.ViewHolder) mActivity
- .getBrowseTestFragment().getRowsFragment().getRowViewHolder(selectRow);
- assertNotNull(dumpRecyclerView(mActivity.getBrowseTestFragment().getGridView()), row);
- assertNotNull(row.getGridView());
- assertEquals(selectItem, row.getGridView().getSelectedPosition());
- }
- });
- }
-
- @Test
- public void activityRecreate_notCrash() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD, true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- InstrumentationRegistry.getInstrumentation().callActivityOnRestart(mActivity);
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.recreate();
- }
- });
- }
-
-
- @Test
- public void lateLoadingHeaderDisabled() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseFragmentTestActivity.EXTRA_HEADERS_STATE,
- BrowseFragment.HEADERS_DISABLED);
- mActivity = activityTestRule.launchActivity(intent);
- waitForEntranceTransitionFinished();
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mActivity.getBrowseTestFragment().getGridView() != null
- && mActivity.getBrowseTestFragment().getGridView().getChildCount() > 0;
- }
- });
- }
-
- private void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
- public static class ItemSelectionTask extends Presenter.ViewHolderTask {
-
- private final BrowseFragmentTestActivity activity;
- private final int expectedRow;
-
- public ItemSelectionTask(BrowseFragmentTestActivity activity, int expectedRow) {
- this.activity = activity;
- this.expectedRow = expectedRow;
- }
-
- @Override
- public void run(Presenter.ViewHolder holder) {
- android.util.Log.d(TAG, dumpRecyclerView(activity.getBrowseTestFragment()
- .getGridView()));
- android.util.Log.d(TAG, "Row " + expectedRow + " " + activity.getBrowseTestFragment()
- .getRowsFragment().getRowViewHolder(expectedRow), new Exception());
- }
- }
-
- static String dumpRecyclerView(RecyclerView recyclerView) {
- StringBuffer b = new StringBuffer();
- for (int i = 0; i < recyclerView.getChildCount(); i++) {
- View child = recyclerView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- recyclerView.getChildViewHolder(child);
- b.append("child").append(i).append(":").append(vh);
- if (vh != null) {
- b.append(",").append(vh.getViewHolder());
- }
- b.append(";");
- }
- return b.toString();
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
deleted file mode 100644
index 605a9ca..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-public class BrowseFragmentTestActivity extends Activity {
-
- public static final String EXTRA_ADD_TO_BACKSTACK = "addToBackStack";
- public static final String EXTRA_NUM_ROWS = "numRows";
- public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
- public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
- public static final String EXTRA_TEST_ENTRANCE_TRANSITION = "testEntranceTransition";
- public static final String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
- public static final String EXTRA_HEADERS_STATE = "headers_state";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Intent intent = getIntent();
-
- setContentView(R.layout.browse);
- if (savedInstanceState == null) {
- Bundle arguments = new Bundle();
- arguments.putAll(intent.getExtras());
- BrowseTestFragment fragment = new BrowseTestFragment();
- fragment.setArguments(arguments);
- FragmentTransaction ft = getFragmentManager().beginTransaction();
- ft.replace(R.id.main_frame, fragment);
- if (intent.getBooleanExtra(EXTRA_ADD_TO_BACKSTACK, false)) {
- ft.addToBackStack(null);
- }
- ft.commit();
- }
- }
-
- public BrowseTestFragment getBrowseTestFragment() {
- return (BrowseTestFragment) getFragmentManager().findFragmentById(R.id.main_frame);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
deleted file mode 100644
index f578874..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
+++ /dev/null
@@ -1,257 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * 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 android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
-import android.content.Intent;
-import android.os.Build;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v7.widget.RecyclerView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class BrowseSupportFragmentTest {
-
- static final String TAG = "BrowseSupportFragmentTest";
- static final long WAIT_TRANSIITON_TIMEOUT = 10000;
-
- @Rule
- public ActivityTestRule<BrowseSupportFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(BrowseSupportFragmentTestActivity.class, false, false);
- private BrowseSupportFragmentTestActivity mActivity;
-
- @After
- public void afterTest() throws Throwable {
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (mActivity != null) {
- mActivity.finish();
- mActivity = null;
- }
- }
- });
- }
-
- void waitForEntranceTransitionFinished() {
- PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- if (Build.VERSION.SDK_INT >= 21) {
- return mActivity.getBrowseTestSupportFragment() != null
- && mActivity.getBrowseTestSupportFragment().mEntranceTransitionEnded;
- } else {
- // when entrance transition not supported, wait main fragment loaded.
- return mActivity.getBrowseTestSupportFragment() != null
- && mActivity.getBrowseTestSupportFragment().getMainFragment() != null;
- }
- }
- });
- }
-
- void waitForHeaderTransitionFinished() {
- View row = mActivity.getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(
- mActivity.getBrowseTestSupportFragment().getSelectedPosition()).view;
- PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.ViewStableOnScreen(row));
- }
-
- @Test
- public void testTwoBackKeysWithBackStack() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- waitForHeaderTransitionFinished();
- sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
- }
-
- @Test
- public void testTwoBackKeysWithoutBackStack() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- waitForHeaderTransitionFinished();
- sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
- }
-
- @Test
- public void testPressRightBeforeMainFragmentCreated() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- mActivity = activityTestRule.launchActivity(intent);
-
- assertNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
- sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
- }
-
- @Test
- public void testSelectCardOnARow() throws Throwable {
- final int selectRow = 10;
- final int selectItem = 20;
- Intent intent = new Intent();
- final long dataLoadingDelay = 1000;
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- Presenter.ViewHolderTask itemTask = Mockito.spy(
- new ItemSelectionTask(mActivity, selectRow));
-
- final ListRowPresenter.SelectItemViewHolderTask task =
- new ListRowPresenter.SelectItemViewHolderTask(selectItem);
- task.setItemTask(itemTask);
-
- mActivity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.getBrowseTestSupportFragment().setSelectedPosition(selectRow, true, task);
- }
- });
-
- verify(itemTask, timeout(5000).times(1)).run(any(Presenter.ViewHolder.class));
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ListRowPresenter.ViewHolder row = (ListRowPresenter.ViewHolder) mActivity
- .getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(selectRow);
- assertNotNull(dumpRecyclerView(mActivity.getBrowseTestSupportFragment().getGridView()), row);
- assertNotNull(row.getGridView());
- assertEquals(selectItem, row.getGridView().getSelectedPosition());
- }
- });
- }
-
- @Test
- public void activityRecreate_notCrash() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD, true);
- mActivity = activityTestRule.launchActivity(intent);
-
- waitForEntranceTransitionFinished();
-
- InstrumentationRegistry.getInstrumentation().callActivityOnRestart(mActivity);
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.recreate();
- }
- });
- }
-
-
- @Test
- public void lateLoadingHeaderDisabled() throws Throwable {
- final long dataLoadingDelay = 1000;
- Intent intent = new Intent();
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
- intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE,
- BrowseSupportFragment.HEADERS_DISABLED);
- mActivity = activityTestRule.launchActivity(intent);
- waitForEntranceTransitionFinished();
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mActivity.getBrowseTestSupportFragment().getGridView() != null
- && mActivity.getBrowseTestSupportFragment().getGridView().getChildCount() > 0;
- }
- });
- }
-
- private void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
- public static class ItemSelectionTask extends Presenter.ViewHolderTask {
-
- private final BrowseSupportFragmentTestActivity activity;
- private final int expectedRow;
-
- public ItemSelectionTask(BrowseSupportFragmentTestActivity activity, int expectedRow) {
- this.activity = activity;
- this.expectedRow = expectedRow;
- }
-
- @Override
- public void run(Presenter.ViewHolder holder) {
- android.util.Log.d(TAG, dumpRecyclerView(activity.getBrowseTestSupportFragment()
- .getGridView()));
- android.util.Log.d(TAG, "Row " + expectedRow + " " + activity.getBrowseTestSupportFragment()
- .getRowsSupportFragment().getRowViewHolder(expectedRow), new Exception());
- }
- }
-
- static String dumpRecyclerView(RecyclerView recyclerView) {
- StringBuffer b = new StringBuffer();
- for (int i = 0; i < recyclerView.getChildCount(); i++) {
- View child = recyclerView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- recyclerView.getChildViewHolder(child);
- b.append("child").append(i).append(":").append(vh);
- if (vh != null) {
- b.append(",").append(vh.getViewHolder());
- }
- b.append(";");
- }
- return b.toString();
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
deleted file mode 100644
index 9df846f..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseFragmentTestActivity.java. DO NOT MODIFY. */
-
-/*
- * 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 android.support.v17.leanback.app;
-
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-public class BrowseSupportFragmentTestActivity extends FragmentActivity {
-
- public static final String EXTRA_ADD_TO_BACKSTACK = "addToBackStack";
- public static final String EXTRA_NUM_ROWS = "numRows";
- public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
- public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
- public static final String EXTRA_TEST_ENTRANCE_TRANSITION = "testEntranceTransition";
- public static final String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
- public static final String EXTRA_HEADERS_STATE = "headers_state";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Intent intent = getIntent();
-
- setContentView(R.layout.browse);
- if (savedInstanceState == null) {
- Bundle arguments = new Bundle();
- arguments.putAll(intent.getExtras());
- BrowseTestSupportFragment fragment = new BrowseTestSupportFragment();
- fragment.setArguments(arguments);
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- ft.replace(R.id.main_frame, fragment);
- if (intent.getBooleanExtra(EXTRA_ADD_TO_BACKSTACK, false)) {
- ft.addToBackStack(null);
- }
- ft.commit();
- }
- }
-
- public BrowseTestSupportFragment getBrowseTestSupportFragment() {
- return (BrowseTestSupportFragment) getSupportFragmentManager().findFragmentById(R.id.main_frame);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
deleted file mode 100644
index 4fe79f0..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package android.support.v17.leanback.app;
-
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_HEADERS_STATE;
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_NUM_ROWS;
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
-import static android.support.v17.leanback.app.BrowseFragmentTestActivity.EXTRA_TEST_ENTRANCE_TRANSITION;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.util.Log;
-import android.view.View;
-
-public class BrowseTestFragment extends BrowseFragment {
- private static final String TAG = "BrowseTestFragment";
-
- final static int DEFAULT_NUM_ROWS = 100;
- final static int DEFAULT_REPEAT_PER_ROW = 20;
- final static long DEFAULT_LOAD_DATA_DELAY = 2000;
- final static boolean DEFAULT_TEST_ENTRANCE_TRANSITION = true;
- final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
-
- private ArrayObjectAdapter mRowsAdapter;
-
- // For good performance, it's important to use a single instance of
- // a card presenter for all rows using that presenter.
- final static StringPresenter sCardPresenter = new StringPresenter();
-
- int NUM_ROWS;
- int REPEAT_PER_ROW;
- boolean mEntranceTransitionStarted;
- boolean mEntranceTransitionEnded;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Log.i(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- Bundle arguments = getArguments();
- NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, BrowseTestFragment.DEFAULT_NUM_ROWS);
- REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
- DEFAULT_REPEAT_PER_ROW);
- long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
- DEFAULT_LOAD_DATA_DELAY);
- boolean TEST_ENTRANCE_TRANSITION = arguments.getBoolean(
- EXTRA_TEST_ENTRANCE_TRANSITION,
- DEFAULT_TEST_ENTRANCE_TRANSITION);
- final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
- EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
- DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
-
- if (!SET_ADAPTER_AFTER_DATA_LOAD) {
- setupRows();
- }
-
- setTitle("BrowseTestFragment");
- setHeadersState(arguments.getInt(EXTRA_HEADERS_STATE, HEADERS_ENABLED));
-
- setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Log.i(TAG, "onSearchClicked");
- }
- });
-
- setOnItemViewClickedListener(new ItemViewClickedListener());
- setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
- + " " + rowViewHolder
- + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
- }
- });
- if (TEST_ENTRANCE_TRANSITION) {
- // don't run entrance transition if fragment is restored.
- if (savedInstanceState == null) {
- prepareEntranceTransition();
- }
- }
- // simulates in a real world use case data being loaded two seconds later
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null || getActivity().isDestroyed()) {
- return;
- }
- if (SET_ADAPTER_AFTER_DATA_LOAD) {
- setupRows();
- }
- loadData();
- startEntranceTransition();
- }
- }, LOAD_DATA_DELAY);
- }
-
- private void setupRows() {
- ListRowPresenter lrp = new ListRowPresenter();
-
- mRowsAdapter = new ArrayObjectAdapter(lrp);
-
- setAdapter(mRowsAdapter);
- }
-
- @Override
- protected void onEntranceTransitionStart() {
- super.onEntranceTransitionStart();
- mEntranceTransitionStarted = true;
- }
-
- @Override
- protected void onEntranceTransitionEnd() {
- super.onEntranceTransitionEnd();
- mEntranceTransitionEnded = true;
- }
-
- private void loadData() {
- for (int i = 0; i < NUM_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
- int index = 0;
- for (int j = 0; j < REPEAT_PER_ROW; ++j) {
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("This is a test-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("GuidedStepFragment-" + (index++));
- }
- HeaderItem header = new HeaderItem(i, "Row " + i);
- mRowsAdapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- private final class ItemViewClickedListener implements OnItemViewClickedListener {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.i(TAG, "onItemClicked: " + item + " row " + row);
- }
- }
-
- public VerticalGridView getGridView() {
- return getRowsFragment().getVerticalGridView();
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
deleted file mode 100644
index 2acc530..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
+++ /dev/null
@@ -1,175 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseTestFragment.java. DO NOT MODIFY. */
-
-/*
- * 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 android.support.v17.leanback.app;
-
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE;
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_NUM_ROWS;
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
-import static android.support.v17.leanback.app.BrowseSupportFragmentTestActivity.EXTRA_TEST_ENTRANCE_TRANSITION;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.util.Log;
-import android.view.View;
-
-public class BrowseTestSupportFragment extends BrowseSupportFragment {
- private static final String TAG = "BrowseTestSupportFragment";
-
- final static int DEFAULT_NUM_ROWS = 100;
- final static int DEFAULT_REPEAT_PER_ROW = 20;
- final static long DEFAULT_LOAD_DATA_DELAY = 2000;
- final static boolean DEFAULT_TEST_ENTRANCE_TRANSITION = true;
- final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
-
- private ArrayObjectAdapter mRowsAdapter;
-
- // For good performance, it's important to use a single instance of
- // a card presenter for all rows using that presenter.
- final static StringPresenter sCardPresenter = new StringPresenter();
-
- int NUM_ROWS;
- int REPEAT_PER_ROW;
- boolean mEntranceTransitionStarted;
- boolean mEntranceTransitionEnded;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Log.i(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- Bundle arguments = getArguments();
- NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, BrowseTestSupportFragment.DEFAULT_NUM_ROWS);
- REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
- DEFAULT_REPEAT_PER_ROW);
- long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
- DEFAULT_LOAD_DATA_DELAY);
- boolean TEST_ENTRANCE_TRANSITION = arguments.getBoolean(
- EXTRA_TEST_ENTRANCE_TRANSITION,
- DEFAULT_TEST_ENTRANCE_TRANSITION);
- final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
- EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
- DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
-
- if (!SET_ADAPTER_AFTER_DATA_LOAD) {
- setupRows();
- }
-
- setTitle("BrowseTestSupportFragment");
- setHeadersState(arguments.getInt(EXTRA_HEADERS_STATE, HEADERS_ENABLED));
-
- setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Log.i(TAG, "onSearchClicked");
- }
- });
-
- setOnItemViewClickedListener(new ItemViewClickedListener());
- setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
- @Override
- public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
- + " " + rowViewHolder
- + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
- }
- });
- if (TEST_ENTRANCE_TRANSITION) {
- // don't run entrance transition if fragment is restored.
- if (savedInstanceState == null) {
- prepareEntranceTransition();
- }
- }
- // simulates in a real world use case data being loaded two seconds later
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null || getActivity().isDestroyed()) {
- return;
- }
- if (SET_ADAPTER_AFTER_DATA_LOAD) {
- setupRows();
- }
- loadData();
- startEntranceTransition();
- }
- }, LOAD_DATA_DELAY);
- }
-
- private void setupRows() {
- ListRowPresenter lrp = new ListRowPresenter();
-
- mRowsAdapter = new ArrayObjectAdapter(lrp);
-
- setAdapter(mRowsAdapter);
- }
-
- @Override
- protected void onEntranceTransitionStart() {
- super.onEntranceTransitionStart();
- mEntranceTransitionStarted = true;
- }
-
- @Override
- protected void onEntranceTransitionEnd() {
- super.onEntranceTransitionEnd();
- mEntranceTransitionEnded = true;
- }
-
- private void loadData() {
- for (int i = 0; i < NUM_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
- int index = 0;
- for (int j = 0; j < REPEAT_PER_ROW; ++j) {
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("This is a test-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
- }
- HeaderItem header = new HeaderItem(i, "Row " + i);
- mRowsAdapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- private final class ItemViewClickedListener implements OnItemViewClickedListener {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.i(TAG, "onItemClicked: " + item + " row " + row);
- }
- }
-
- public VerticalGridView getGridView() {
- return getRowsSupportFragment().getVerticalGridView();
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
deleted file mode 100644
index 38d08c7..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ /dev/null
@@ -1,1216 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.animation.PropertyValuesHolder;
-import android.app.Fragment;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
-import android.support.v17.leanback.media.MediaPlayerGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.util.StateMachine;
-import android.support.v17.leanback.widget.DetailsParallax;
-import android.support.v17.leanback.widget.DetailsParallaxDrawable;
-import android.support.v17.leanback.widget.ParallaxTarget;
-import android.support.v17.leanback.widget.RecyclerViewParallax;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link DetailsFragment}.
- */
-@RunWith(JUnit4.class)
-@LargeTest
-public class DetailsFragmentTest extends SingleFragmentTestBase {
-
- static final int PARALLAX_VERTICAL_OFFSET = -300;
-
- static int getCoverDrawableAlpha(DetailsFragmentBackgroundController controller) {
- return ((FitWidthBitmapDrawable) controller.mParallaxDrawable.getCoverDrawable())
- .getAlpha();
- }
-
- public static class DetailsFragmentParallax extends DetailsTestFragment {
-
- private DetailsParallaxDrawable mParallaxDrawable;
-
- public DetailsFragmentParallax() {
- super();
- mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Drawable coverDrawable = new FitWidthBitmapDrawable();
- mParallaxDrawable = new DetailsParallaxDrawable(
- getActivity(),
- getParallax(),
- coverDrawable,
- new ParallaxTarget.PropertyValuesHolderTarget(
- coverDrawable,
- PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
- )
- );
-
- BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
- backgroundManager.attach(getActivity().getWindow());
- backgroundManager.setDrawable(mParallaxDrawable);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
-
- @Override
- public void onResume() {
- super.onResume();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
- }
-
- DetailsParallaxDrawable getParallaxDrawable() {
- return mParallaxDrawable;
- }
- }
-
- @Test
- public void parallaxSetupTest() {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentTest.DetailsFragmentParallax.class,
- new SingleFragmentTestBase.Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- double delta = 0.0002;
- DetailsParallax dpm = ((DetailsFragment) activity.getTestFragment()).getParallax();
-
- RecyclerViewParallax.ChildPositionProperty frameTop =
- (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
- assertEquals(0f, frameTop.getFraction(), delta);
- assertEquals(0f, frameTop.getAdapterPosition(), delta);
-
-
- RecyclerViewParallax.ChildPositionProperty frameBottom =
- (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
- assertEquals(1f, frameBottom.getFraction(), delta);
- assertEquals(0f, frameBottom.getAdapterPosition(), delta);
- }
-
- @Test
- public void parallaxTest() throws Throwable {
- SingleFragmentTestActivity activity = launchAndWaitActivity(DetailsFragmentParallax.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsFragmentParallax detailsFragment =
- (DetailsFragmentParallax) activity.getTestFragment();
- DetailsParallaxDrawable drawable =
- detailsFragment.getParallaxDrawable();
- final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
- drawable.getCoverDrawable();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsFragment().getAdapter() != null
- && detailsFragment.getRowsFragment().getAdapter().size() > 1;
- }
- });
-
- final VerticalGridView verticalGridView = detailsFragment.getRowsFragment()
- .getVerticalGridView();
- final int windowHeight = verticalGridView.getHeight();
- final int windowWidth = verticalGridView.getWidth();
- // make sure background manager attached to window is same size as VerticalGridView
- // i.e. no status bar.
- assertEquals(windowHeight, activity.getWindow().getDecorView().getHeight());
- assertEquals(windowWidth, activity.getWindow().getDecorView().getWidth());
-
- final View detailsFrame = verticalGridView.findViewById(R.id.details_frame);
-
- assertEquals(windowWidth, bitmapDrawable.getBounds().width());
-
- final Rect detailsFrameRect = new Rect();
- detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
- verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
-
- assertEquals(Math.min(windowHeight, detailsFrameRect.top),
- bitmapDrawable.getBounds().height());
- assertEquals(0, bitmapDrawable.getVerticalOffset());
-
- assertTrue("TitleView is visible", detailsFragment.getView()
- .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- verticalGridView.scrollToPosition(1);
- }
- });
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
- && detailsFragment.getView()
- .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
- }
- });
-
- detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
- verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
-
- assertEquals(0, bitmapDrawable.getBounds().top);
- assertEquals(Math.max(detailsFrameRect.top, 0), bitmapDrawable.getBounds().bottom);
- assertEquals(windowWidth, bitmapDrawable.getBounds().width());
-
- ColorDrawable colorDrawable = (ColorDrawable) (drawable.getChildAt(1).getDrawable());
- assertEquals(windowWidth, colorDrawable.getBounds().width());
- assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
- assertEquals(windowHeight, colorDrawable.getBounds().bottom);
- }
-
- public static class DetailsFragmentWithVideo extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
- MediaPlayerGlue mGlue;
-
- public DetailsFragmentWithVideo() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mGlue = new MediaPlayerGlue(getActivity());
- mDetailsBackground.setupVideoPlayback(mGlue);
-
- mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mGlue.setArtist("A Googleer");
- mGlue.setTitle("Diving with Sharks");
- mGlue.setMediaSource(
- Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- public static class DetailsFragmentWithVideo1 extends DetailsFragmentWithVideo {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
-
- public static class DetailsFragmentWithVideo2 extends DetailsFragmentWithVideo {
-
- @Override
- public void onStart() {
- super.onStart();
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
-
- private void navigateBetweenRowsAndVideoUsingRequestFocusInternal(Class cls)
- throws Throwable {
- SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsFragmentWithVideo detailsFragment =
- (DetailsFragmentWithVideo) activity.getTestFragment();
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mVideoFragment.getView().requestFocus();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
- assertFalse(detailsFragment.isShowingTitle());
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.getRowsFragment().getVerticalGridView().requestFocus();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() == originalFirstRowTop;
- }
- });
- assertTrue(detailsFragment.isShowingTitle());
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingRequestFocus1() throws Throwable {
- navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsFragmentWithVideo1.class);
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingRequestFocus2() throws Throwable {
- navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsFragmentWithVideo2.class);
- }
-
- private void navigateBetweenRowsAndVideoUsingDPADInternal(Class cls) throws Throwable {
- SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsFragmentWithVideo detailsFragment =
- (DetailsFragmentWithVideo) activity.getTestFragment();
- // wait video playing
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- // navigate to video
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
-
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
- }
- });
-
- // navigate to details
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() == originalFirstRowTop;
- }
- });
- assertTrue(detailsFragment.isShowingTitle());
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingDPAD1() throws Throwable {
- navigateBetweenRowsAndVideoUsingDPADInternal(DetailsFragmentWithVideo1.class);
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingDPAD2() throws Throwable {
- navigateBetweenRowsAndVideoUsingDPADInternal(DetailsFragmentWithVideo2.class);
- }
-
- public static class EmptyFragmentClass extends Fragment {
- @Override
- public void onStart() {
- super.onStart();
- getActivity().finish();
- }
- }
-
- private void fragmentOnStartWithVideoInternal(Class cls) throws Throwable {
- final SingleFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsFragmentWithVideo detailsFragment =
- (DetailsFragmentWithVideo) activity.getTestFragment();
- // wait video playing
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- // navigate to video
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
-
- // start an empty activity
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- Intent intent = new Intent(activity, SingleFragmentTestActivity.class);
- intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME,
- EmptyFragmentClass.class.getName());
- activity.startActivity(intent);
- }
- }
- );
- PollingCheck.waitFor(2000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.isResumed();
- }
- });
- assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
- }
-
- @Test
- public void fragmentOnStartWithVideo1() throws Throwable {
- fragmentOnStartWithVideoInternal(DetailsFragmentWithVideo1.class);
- }
-
- @Test
- public void fragmentOnStartWithVideo2() throws Throwable {
- fragmentOnStartWithVideoInternal(DetailsFragmentWithVideo2.class);
- }
-
- @Test
- public void navigateBetweenRowsAndTitle() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsTestFragment.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsTestFragment detailsFragment =
- (DetailsTestFragment) activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- }
- });
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- });
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(detailsFragment.getTitleView().hasFocus());
- assertEquals(originalFirstRowTop, firstRow.getTop());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.hasFocus());
- assertEquals(originalFirstRowTop, firstRow.getTop());
- }
-
- public static class DetailsFragmentWithNoVideo extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
-
- public DetailsFragmentWithNoVideo() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
-
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void lateSetupVideo() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentWithNoVideo detailsFragment =
- (DetailsFragmentWithNoVideo) activity.getTestFragment();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- assertTrue(firstRow.hasFocus());
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // after setup Video Playback the DPAD up will navigate to Video Fragment.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null;
- }
- });
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
- .getPlaybackGlue()).isMediaPlaying();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
-
- // wait a little bit to replace with new Glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
- glue2.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue2.setArtist("A Googleer");
- glue2.setTitle("Diving with Sharks");
- glue2.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // test switchToRows() and switchToVideo()
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToRows();
- }
- }
- );
- assertTrue(detailsFragment.mRowsFragment.getView().hasFocus());
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- }
- );
- assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- }
-
- @Test
- public void sharedGlueHost() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentWithNoVideo detailsFragment =
- (DetailsFragmentWithNoVideo) activity.getTestFragment();
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue1 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue1);
- glue1.setArtist("A Googleer");
- glue1.setTitle("Diving with Sharks");
- glue1.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // after setup Video Playback the DPAD up will navigate to Video Fragment.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null;
- }
- });
-
- final MediaPlayerGlue glue1 = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
- PlaybackGlueHost playbackGlueHost = glue1.getHost();
-
- // wait a little bit to replace with new Glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
- glue2.setArtist("A Googleer");
- glue2.setTitle("Diving with Sharks");
- glue2.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // wait for new glue to get its glue host
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- MediaPlayerGlue mediaPlayerGlue = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
- return mediaPlayerGlue != null && mediaPlayerGlue != glue1
- && mediaPlayerGlue.getHost() != null;
- }
- });
-
- final MediaPlayerGlue glue2 = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
-
- assertTrue(glue1.getHost() == null);
- assertTrue(glue2.getHost() == playbackGlueHost);
- }
-
- @Test
- public void clearVideo() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentWithNoVideo detailsFragment =
- (DetailsFragmentWithNoVideo) activity.getTestFragment();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsFragment().getVerticalGridView().getChildAt(0);
- final int screenHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
- .getPlaybackGlue()).isMediaPlaying();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
-
- // wait a little bit then reset glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(null);
- }
- }
- );
- // background should fade in upon reset playback
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 255 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
- }
-
- public static class DetailsFragmentWithNoItem extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
-
- public DetailsFragmentWithNoItem() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void noInitialItem() {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentWithNoItem.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentWithNoItem detailsFragment =
- (DetailsFragmentWithNoItem) activity.getTestFragment();
-
- final int recyclerViewHeight = detailsFragment.getRowsFragment().getVerticalGridView()
- .getHeight();
- assertTrue(recyclerViewHeight > 0);
-
- assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
- assertEquals(0, coverDrawable.getBounds().top);
- assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
- Drawable bottomDrawable = detailsFragment.mDetailsBackgroundController.getBottomDrawable();
- assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
- assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
- }
-
- public static class DetailsFragmentSwitchToVideoInOnCreate extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
-
- public DetailsFragmentSwitchToVideoInOnCreate() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mDetailsBackground.switchToVideo();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void switchToVideoInOnCreate() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
- (DetailsFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
-
- // the pending enter transition flag should be automatically cleared
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
- assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
- assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
- //SystemClock.sleep(5000);
- assertFalse(detailsFragment.isShowingTitle());
-
- SystemClock.sleep(1000);
- assertNull(detailsFragment.mVideoFragment);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
- // once the video fragment is created it would be immediately assigned focus
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null
- && detailsFragment.mVideoFragment.getView().hasFocus();
- }
- });
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
- }
- });
-
- // switchToRows does nothing if there is no row
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToRows();
- }
- }
- );
- assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
-
- // create item, it should be layout outside screen
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world",
- "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
- );
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getVerticalGridView().getChildCount() > 0
- && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
- >= detailsFragment.getVerticalGridView().getHeight();
- }
- });
-
- // pressing BACK will return to details row
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
- < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
- }
- });
- assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
- }
-
- @Test
- public void switchToVideoBackToQuit() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
- (DetailsFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
-
- // the pending enter transition flag should be automatically cleared
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
- assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
- assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
- assertFalse(detailsFragment.isShowingTitle());
-
- SystemClock.sleep(1000);
- assertNull(detailsFragment.mVideoFragment);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
- // once the video fragment is created it would be immediately assigned focus
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoFragment != null
- && detailsFragment.mVideoFragment.getView() != null
- && detailsFragment.mVideoFragment.getView().hasFocus();
- }
- });
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
- }
- });
-
- // before any details row is presented, pressing BACK will quit the activity
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(activity));
- }
-
- public static class DetailsFragmentSwitchToVideoAndPrepareEntranceTransition
- extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
-
- public DetailsFragmentSwitchToVideoAndPrepareEntranceTransition() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mDetailsBackground.switchToVideo();
- prepareEntranceTransition();
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(
- DetailsFragmentSwitchToVideoAndPrepareEntranceTransition.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
- (DetailsFragmentSwitchToVideoAndPrepareEntranceTransition)
- activity.getTestFragment();
-
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
- }
-
- public static class DetailsFragmentEntranceTransition
- extends DetailsTestFragment {
-
- final DetailsFragmentBackgroundController mDetailsBackground =
- new DetailsFragmentBackgroundController(this);
-
- public DetailsFragmentEntranceTransition() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- prepareEntranceTransition();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void entranceTransitionBlocksSwitchToVideo() {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(DetailsFragmentEntranceTransition.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsFragmentEntranceTransition detailsFragment =
- (DetailsFragmentEntranceTransition)
- activity.getTestFragment();
-
- if (Build.VERSION.SDK_INT < 21) {
- // when enter transition is not supported, mCanUseHost is immmediately true
- assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
- } else {
- // calling switchToVideo() between prepareEntranceTransition and entrance transition
- // finishes will be ignored.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- });
- assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
- }
- assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- detailsFragment.startEntranceTransition();
- }
- });
- // once Entrance transition is finished, mCanUseHost will be true
- // and we can switchToVideo and fade out the background.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mDetailsBackgroundController.mCanUseHost;
- }
- });
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
- }
-
- public static class DetailsFragmentEntranceTransitionTimeout extends DetailsTestFragment {
-
- public DetailsFragmentEntranceTransitionTimeout() {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- prepareEntranceTransition();
- }
-
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- public void startEntranceTransitionAfterDestroyed() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(
- DetailsFragmentEntranceTransition.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN),
- 1000);
- final DetailsFragmentEntranceTransition detailsFragment =
- (DetailsFragmentEntranceTransition)
- activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- });
- SystemClock.sleep(100);
- activity.finish();
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.startEntranceTransition();
- }
- });
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
deleted file mode 100644
index 04f20bc..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
+++ /dev/null
@@ -1,1219 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.animation.PropertyValuesHolder;
-import android.support.v4.app.Fragment;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
-import android.support.v17.leanback.media.MediaPlayerGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.transition.TransitionHelper;
-import android.support.v17.leanback.util.StateMachine;
-import android.support.v17.leanback.widget.DetailsParallax;
-import android.support.v17.leanback.widget.DetailsParallaxDrawable;
-import android.support.v17.leanback.widget.ParallaxTarget;
-import android.support.v17.leanback.widget.RecyclerViewParallax;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link DetailsSupportFragment}.
- */
-@RunWith(JUnit4.class)
-@LargeTest
-public class DetailsSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- static final int PARALLAX_VERTICAL_OFFSET = -300;
-
- static int getCoverDrawableAlpha(DetailsSupportFragmentBackgroundController controller) {
- return ((FitWidthBitmapDrawable) controller.mParallaxDrawable.getCoverDrawable())
- .getAlpha();
- }
-
- public static class DetailsSupportFragmentParallax extends DetailsTestSupportFragment {
-
- private DetailsParallaxDrawable mParallaxDrawable;
-
- public DetailsSupportFragmentParallax() {
- super();
- mMinVerticalOffset = PARALLAX_VERTICAL_OFFSET;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Drawable coverDrawable = new FitWidthBitmapDrawable();
- mParallaxDrawable = new DetailsParallaxDrawable(
- getActivity(),
- getParallax(),
- coverDrawable,
- new ParallaxTarget.PropertyValuesHolderTarget(
- coverDrawable,
- PropertyValuesHolder.ofInt("verticalOffset", 0, mMinVerticalOffset)
- )
- );
-
- BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
- backgroundManager.attach(getActivity().getWindow());
- backgroundManager.setDrawable(mParallaxDrawable);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
-
- @Override
- public void onResume() {
- super.onResume();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- ((FitWidthBitmapDrawable) mParallaxDrawable.getCoverDrawable()).setBitmap(bitmap);
- }
-
- DetailsParallaxDrawable getParallaxDrawable() {
- return mParallaxDrawable;
- }
- }
-
- @Test
- public void parallaxSetupTest() {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentTest.DetailsSupportFragmentParallax.class,
- new SingleSupportFragmentTestBase.Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- double delta = 0.0002;
- DetailsParallax dpm = ((DetailsSupportFragment) activity.getTestFragment()).getParallax();
-
- RecyclerViewParallax.ChildPositionProperty frameTop =
- (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowTop();
- assertEquals(0f, frameTop.getFraction(), delta);
- assertEquals(0f, frameTop.getAdapterPosition(), delta);
-
-
- RecyclerViewParallax.ChildPositionProperty frameBottom =
- (RecyclerViewParallax.ChildPositionProperty) dpm.getOverviewRowBottom();
- assertEquals(1f, frameBottom.getFraction(), delta);
- assertEquals(0f, frameBottom.getAdapterPosition(), delta);
- }
-
- @Test
- public void parallaxTest() throws Throwable {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(DetailsSupportFragmentParallax.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsSupportFragmentParallax detailsFragment =
- (DetailsSupportFragmentParallax) activity.getTestFragment();
- DetailsParallaxDrawable drawable =
- detailsFragment.getParallaxDrawable();
- final FitWidthBitmapDrawable bitmapDrawable = (FitWidthBitmapDrawable)
- drawable.getCoverDrawable();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsSupportFragment().getAdapter() != null
- && detailsFragment.getRowsSupportFragment().getAdapter().size() > 1;
- }
- });
-
- final VerticalGridView verticalGridView = detailsFragment.getRowsSupportFragment()
- .getVerticalGridView();
- final int windowHeight = verticalGridView.getHeight();
- final int windowWidth = verticalGridView.getWidth();
- // make sure background manager attached to window is same size as VerticalGridView
- // i.e. no status bar.
- assertEquals(windowHeight, activity.getWindow().getDecorView().getHeight());
- assertEquals(windowWidth, activity.getWindow().getDecorView().getWidth());
-
- final View detailsFrame = verticalGridView.findViewById(R.id.details_frame);
-
- assertEquals(windowWidth, bitmapDrawable.getBounds().width());
-
- final Rect detailsFrameRect = new Rect();
- detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
- verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
-
- assertEquals(Math.min(windowHeight, detailsFrameRect.top),
- bitmapDrawable.getBounds().height());
- assertEquals(0, bitmapDrawable.getVerticalOffset());
-
- assertTrue("TitleView is visible", detailsFragment.getView()
- .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- verticalGridView.scrollToPosition(1);
- }
- });
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return bitmapDrawable.getVerticalOffset() == PARALLAX_VERTICAL_OFFSET
- && detailsFragment.getView()
- .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
- }
- });
-
- detailsFrameRect.set(0, 0, detailsFrame.getWidth(), detailsFrame.getHeight());
- verticalGridView.offsetDescendantRectToMyCoords(detailsFrame, detailsFrameRect);
-
- assertEquals(0, bitmapDrawable.getBounds().top);
- assertEquals(Math.max(detailsFrameRect.top, 0), bitmapDrawable.getBounds().bottom);
- assertEquals(windowWidth, bitmapDrawable.getBounds().width());
-
- ColorDrawable colorDrawable = (ColorDrawable) (drawable.getChildAt(1).getDrawable());
- assertEquals(windowWidth, colorDrawable.getBounds().width());
- assertEquals(detailsFrameRect.bottom, colorDrawable.getBounds().top);
- assertEquals(windowHeight, colorDrawable.getBounds().bottom);
- }
-
- public static class DetailsSupportFragmentWithVideo extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
- MediaPlayerGlue mGlue;
-
- public DetailsSupportFragmentWithVideo() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mGlue = new MediaPlayerGlue(getActivity());
- mDetailsBackground.setupVideoPlayback(mGlue);
-
- mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mGlue.setArtist("A Googleer");
- mGlue.setTitle("Diving with Sharks");
- mGlue.setMediaSource(
- Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- public static class DetailsSupportFragmentWithVideo1 extends DetailsSupportFragmentWithVideo {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
-
- public static class DetailsSupportFragmentWithVideo2 extends DetailsSupportFragmentWithVideo {
-
- @Override
- public void onStart() {
- super.onStart();
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
-
- private void navigateBetweenRowsAndVideoUsingRequestFocusInternal(Class cls)
- throws Throwable {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsSupportFragmentWithVideo detailsFragment =
- (DetailsSupportFragmentWithVideo) activity.getTestFragment();
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mVideoSupportFragment.getView().requestFocus();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
- assertFalse(detailsFragment.isShowingTitle());
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.getRowsSupportFragment().getVerticalGridView().requestFocus();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() == originalFirstRowTop;
- }
- });
- assertTrue(detailsFragment.isShowingTitle());
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingRequestFocus1() throws Throwable {
- navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsSupportFragmentWithVideo1.class);
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingRequestFocus2() throws Throwable {
- navigateBetweenRowsAndVideoUsingRequestFocusInternal(DetailsSupportFragmentWithVideo2.class);
- }
-
- private void navigateBetweenRowsAndVideoUsingDPADInternal(Class cls) throws Throwable {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsSupportFragmentWithVideo detailsFragment =
- (DetailsSupportFragmentWithVideo) activity.getTestFragment();
- // wait video playing
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- // navigate to video
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
-
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
- }
- });
-
- // navigate to details
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() == originalFirstRowTop;
- }
- });
- assertTrue(detailsFragment.isShowingTitle());
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingDPAD1() throws Throwable {
- navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo1.class);
- }
-
- @Test
- public void navigateBetweenRowsAndVideoUsingDPAD2() throws Throwable {
- navigateBetweenRowsAndVideoUsingDPADInternal(DetailsSupportFragmentWithVideo2.class);
- }
-
- public static class EmptyFragmentClass extends Fragment {
- @Override
- public void onStart() {
- super.onStart();
- getActivity().finish();
- }
- }
-
- private void fragmentOnStartWithVideoInternal(Class cls) throws Throwable {
- final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(cls,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
-
- final DetailsSupportFragmentWithVideo detailsFragment =
- (DetailsSupportFragmentWithVideo) activity.getTestFragment();
- // wait video playing
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null
- && detailsFragment.mGlue.isMediaPlaying();
- }
- });
-
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- assertTrue(firstRow.hasFocus());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
- assertTrue(detailsFragment.isShowingTitle());
-
- // navigate to video
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return firstRow.getTop() >= screenHeight;
- }
- });
-
- // start an empty activity
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- Intent intent = new Intent(activity, SingleSupportFragmentTestActivity.class);
- intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_FRAGMENT_NAME,
- EmptyFragmentClass.class.getName());
- activity.startActivity(intent);
- }
- }
- );
- PollingCheck.waitFor(2000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.isResumed();
- }
- });
- assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
- }
-
- @Test
- public void fragmentOnStartWithVideo1() throws Throwable {
- fragmentOnStartWithVideoInternal(DetailsSupportFragmentWithVideo1.class);
- }
-
- @Test
- public void fragmentOnStartWithVideo2() throws Throwable {
- fragmentOnStartWithVideoInternal(DetailsSupportFragmentWithVideo2.class);
- }
-
- @Test
- public void navigateBetweenRowsAndTitle() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsTestSupportFragment.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsTestSupportFragment detailsFragment =
- (DetailsTestSupportFragment) activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- }
- });
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- });
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int originalFirstRowTop = firstRow.getTop();
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(detailsFragment.getTitleView().hasFocus());
- assertEquals(originalFirstRowTop, firstRow.getTop());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.hasFocus());
- assertEquals(originalFirstRowTop, firstRow.getTop());
- }
-
- public static class DetailsSupportFragmentWithNoVideo extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
-
- public DetailsSupportFragmentWithNoVideo() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
-
- setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void lateSetupVideo() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentWithNoVideo detailsFragment =
- (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- assertTrue(firstRow.hasFocus());
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // after setup Video Playback the DPAD up will navigate to Video Fragment.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null;
- }
- });
- sendKeys(KeyEvent.KEYCODE_DPAD_UP);
- assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
- .getPlaybackGlue()).isMediaPlaying();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
-
- // wait a little bit to replace with new Glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
- glue2.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue2.setArtist("A Googleer");
- glue2.setTitle("Diving with Sharks");
- glue2.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // test switchToRows() and switchToVideo()
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToRows();
- }
- }
- );
- assertTrue(detailsFragment.mRowsSupportFragment.getView().hasFocus());
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- }
- );
- assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(firstRow));
- }
-
- @Test
- public void sharedGlueHost() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentWithNoVideo detailsFragment =
- (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue1 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue1);
- glue1.setArtist("A Googleer");
- glue1.setTitle("Diving with Sharks");
- glue1.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // after setup Video Playback the DPAD up will navigate to Video Fragment.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null;
- }
- });
-
- final MediaPlayerGlue glue1 = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
- PlaybackGlueHost playbackGlueHost = glue1.getHost();
-
- // wait a little bit to replace with new Glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue2 = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue2);
- glue2.setArtist("A Googleer");
- glue2.setTitle("Diving with Sharks");
- glue2.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- // wait for new glue to get its glue host
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- MediaPlayerGlue mediaPlayerGlue = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
- return mediaPlayerGlue != null && mediaPlayerGlue != glue1
- && mediaPlayerGlue.getHost() != null;
- }
- });
-
- final MediaPlayerGlue glue2 = (MediaPlayerGlue) detailsFragment
- .mDetailsBackgroundController
- .getPlaybackGlue();
-
- assertTrue(glue1.getHost() == null);
- assertTrue(glue2.getHost() == playbackGlueHost);
- }
-
- @Test
- public void clearVideo() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentWithNoVideo.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentWithNoVideo detailsFragment =
- (DetailsSupportFragmentWithNoVideo) activity.getTestFragment();
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildCount() > 0;
- }
- });
- final View firstRow = detailsFragment.getRowsSupportFragment().getVerticalGridView().getChildAt(0);
- final int screenHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
-
- assertTrue(firstRow.hasFocus());
- assertTrue(detailsFragment.isShowingTitle());
- assertTrue(firstRow.getTop() > 0 && firstRow.getTop() < screenHeight);
-
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
-
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((MediaPlayerGlue) detailsFragment.mDetailsBackgroundController
- .getPlaybackGlue()).isMediaPlaying();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
-
- // wait a little bit then reset glue
- SystemClock.sleep(1000);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(null);
- }
- }
- );
- // background should fade in upon reset playback
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 255 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
- }
-
- public static class DetailsSupportFragmentWithNoItem extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
-
- public DetailsSupportFragmentWithNoItem() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void noInitialItem() {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentWithNoItem.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentWithNoItem detailsFragment =
- (DetailsSupportFragmentWithNoItem) activity.getTestFragment();
-
- final int recyclerViewHeight = detailsFragment.getRowsSupportFragment().getVerticalGridView()
- .getHeight();
- assertTrue(recyclerViewHeight > 0);
-
- assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
- assertEquals(0, coverDrawable.getBounds().top);
- assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
- Drawable bottomDrawable = detailsFragment.mDetailsBackgroundController.getBottomDrawable();
- assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
- assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
- }
-
- public static class DetailsSupportFragmentSwitchToVideoInOnCreate extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
-
- public DetailsSupportFragmentSwitchToVideoInOnCreate() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mDetailsBackground.switchToVideo();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void switchToVideoInOnCreate() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
- (DetailsSupportFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
-
- // the pending enter transition flag should be automatically cleared
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
- assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
- assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
- //SystemClock.sleep(5000);
- assertFalse(detailsFragment.isShowingTitle());
-
- SystemClock.sleep(1000);
- assertNull(detailsFragment.mVideoSupportFragment);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
- // once the video fragment is created it would be immediately assigned focus
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null
- && detailsFragment.mVideoSupportFragment.getView().hasFocus();
- }
- });
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
- }
- });
-
- // switchToRows does nothing if there is no row
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToRows();
- }
- }
- );
- assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
-
- // create item, it should be layout outside screen
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world",
- "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- }
- );
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getVerticalGridView().getChildCount() > 0
- && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
- >= detailsFragment.getVerticalGridView().getHeight();
- }
- });
-
- // pressing BACK will return to details row
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
- < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
- }
- });
- assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
- }
-
- @Test
- public void switchToVideoBackToQuit() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
- (DetailsSupportFragmentSwitchToVideoInOnCreate) activity.getTestFragment();
-
- // the pending enter transition flag should be automatically cleared
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
- assertNull(TransitionHelper.getEnterTransition(activity.getWindow()));
- assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
- assertFalse(detailsFragment.isShowingTitle());
-
- SystemClock.sleep(1000);
- assertNull(detailsFragment.mVideoSupportFragment);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- final MediaPlayerGlue glue = new MediaPlayerGlue(activity);
- detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
- glue.setMode(MediaPlayerGlue.REPEAT_ALL);
- glue.setArtist("A Googleer");
- glue.setTitle("Diving with Sharks");
- glue.setMediaSource(Uri.parse(
- "android.resource://android.support.v17.leanback.test/raw/video"));
- }
- }
- );
- // once the video fragment is created it would be immediately assigned focus
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mVideoSupportFragment != null
- && detailsFragment.mVideoSupportFragment.getView() != null
- && detailsFragment.mVideoSupportFragment.getView().hasFocus();
- }
- });
- // wait auto hide play controls done:
- PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
- }
- });
-
- // before any details row is presented, pressing BACK will quit the activity
- sendKeys(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(activity));
- }
-
- public static class DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition
- extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
-
- public DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- mDetailsBackground.switchToVideo();
- prepareEntranceTransition();
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
- DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
- (DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition)
- activity.getTestFragment();
-
- assertEquals(StateMachine.STATUS_INVOKED,
- detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
- }
-
- public static class DetailsSupportFragmentEntranceTransition
- extends DetailsTestSupportFragment {
-
- final DetailsSupportFragmentBackgroundController mDetailsBackground =
- new DetailsSupportFragmentBackgroundController(this);
-
- public DetailsSupportFragmentEntranceTransition() {
- mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsBackground.enableParallax();
- prepareEntranceTransition();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
- android.support.v17.leanback.test.R.drawable.spiderman);
- mDetailsBackground.setCoverBitmap(bitmap);
- }
-
- @Override
- public void onStop() {
- mDetailsBackground.setCoverBitmap(null);
- super.onStop();
- }
- }
-
- @Test
- public void entranceTransitionBlocksSwitchToVideo() {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(DetailsSupportFragmentEntranceTransition.class,
- new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
- final DetailsSupportFragmentEntranceTransition detailsFragment =
- (DetailsSupportFragmentEntranceTransition)
- activity.getTestFragment();
-
- if (Build.VERSION.SDK_INT < 21) {
- // when enter transition is not supported, mCanUseHost is immmediately true
- assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
- } else {
- // calling switchToVideo() between prepareEntranceTransition and entrance transition
- // finishes will be ignored.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- });
- assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
- }
- assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- detailsFragment.startEntranceTransition();
- }
- });
- // once Entrance transition is finished, mCanUseHost will be true
- // and we can switchToVideo and fade out the background.
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return detailsFragment.mDetailsBackgroundController.mCanUseHost;
- }
- });
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.mDetailsBackgroundController.switchToVideo();
- }
- });
- PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
- }
- });
- }
-
- public static class DetailsSupportFragmentEntranceTransitionTimeout extends DetailsTestSupportFragment {
-
- public DetailsSupportFragmentEntranceTransitionTimeout() {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- prepareEntranceTransition();
- }
-
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- public void startEntranceTransitionAfterDestroyed() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
- DetailsSupportFragmentEntranceTransition.class, new Options().uiVisibility(
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN),
- 1000);
- final DetailsSupportFragmentEntranceTransition detailsFragment =
- (DetailsSupportFragmentEntranceTransition)
- activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
- android.support.v17.leanback.test.R.drawable.spiderman));
- }
- });
- SystemClock.sleep(100);
- activity.finish();
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- detailsFragment.startEntranceTransition();
- }
- });
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
deleted file mode 100644
index 354e574..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DetailsOverviewRow;
-import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ImageCardView;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.view.ViewGroup;
-
-/**
- * Base class provides overview row and some related rows.
- */
-public class DetailsTestFragment extends android.support.v17.leanback.app.DetailsFragment {
- private static final int NUM_ROWS = 3;
- private ArrayObjectAdapter mRowsAdapter;
- private PhotoItem mPhotoItem;
- private final Presenter mCardPresenter = new Presenter() {
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
- ImageCardView cardView = new ImageCardView(getActivity());
- cardView.setFocusable(true);
- cardView.setFocusableInTouchMode(true);
- return new ViewHolder(cardView);
- }
-
- @Override
- public void onBindViewHolder(ViewHolder viewHolder, Object item) {
- ImageCardView imageCardView = (ImageCardView) viewHolder.view;
- imageCardView.setTitleText("Android Tv");
- imageCardView.setContentText("Android Tv Production Inc.");
- imageCardView.setMainImageDimensions(313, 176);
- }
-
- @Override
- public void onUnbindViewHolder(ViewHolder viewHolder) {
- }
- };
-
- private static final int ACTION_RENT = 2;
- private static final int ACTION_BUY = 3;
-
- protected long mTimeToLoadOverviewRow = 1000;
- protected long mTimeToLoadRelatedRow = 2000;
-
- private Action mActionRent;
- private Action mActionBuy;
-
- protected int mMinVerticalOffset = -100;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setTitle("Leanback Sample App");
-
- mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
- getResources().getDrawable(R.drawable.ic_action_a));
- mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
-
- ClassPresenterSelector ps = new ClassPresenterSelector();
- FullWidthDetailsOverviewRowPresenter dorPresenter =
- new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
- @Override
- protected void onBindDescription(
- AbstractDetailsDescriptionPresenter.ViewHolder vh, Object item) {
- vh.getTitle().setText("Funny Movie");
- vh.getSubtitle().setText("Android TV Production Inc.");
- vh.getBody().setText("What a great movie!");
- }
- });
-
- ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
- ps.addClassPresenter(ListRow.class, new ListRowPresenter());
- mRowsAdapter = new ArrayObjectAdapter(ps);
- }
-
- public void setItem(PhotoItem photoItem) {
- mPhotoItem = photoItem;
- mRowsAdapter.clear();
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null) {
- return;
- }
- Resources res = getActivity().getResources();
- DetailsOverviewRow dor = new DetailsOverviewRow(mPhotoItem.getTitle());
- dor.setImageDrawable(res.getDrawable(mPhotoItem.getImageResourceId()));
- SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();
- adapter.set(ACTION_RENT, mActionRent);
- adapter.set(ACTION_BUY, mActionBuy);
- dor.setActionsAdapter(adapter);
- mRowsAdapter.add(0, dor);
- setSelectedPosition(0, true);
- }
- }, mTimeToLoadOverviewRow);
-
-
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null) {
- return;
- }
- for (int i = 0; i < NUM_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(mCardPresenter);
- listRowAdapter.add(new PhotoItem("Hello world", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("This is a test", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("Android TV", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("Leanback", R.drawable.spiderman));
- HeaderItem header = new HeaderItem(i, "Row " + i);
- mRowsAdapter.add(new ListRow(header, listRowAdapter));
- }
- }
- }, mTimeToLoadRelatedRow);
-
- setAdapter(mRowsAdapter);
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
deleted file mode 100644
index 7d03a45..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsTestFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DetailsOverviewRow;
-import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ImageCardView;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.view.ViewGroup;
-
-/**
- * Base class provides overview row and some related rows.
- */
-public class DetailsTestSupportFragment extends android.support.v17.leanback.app.DetailsSupportFragment {
- private static final int NUM_ROWS = 3;
- private ArrayObjectAdapter mRowsAdapter;
- private PhotoItem mPhotoItem;
- private final Presenter mCardPresenter = new Presenter() {
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
- ImageCardView cardView = new ImageCardView(getActivity());
- cardView.setFocusable(true);
- cardView.setFocusableInTouchMode(true);
- return new ViewHolder(cardView);
- }
-
- @Override
- public void onBindViewHolder(ViewHolder viewHolder, Object item) {
- ImageCardView imageCardView = (ImageCardView) viewHolder.view;
- imageCardView.setTitleText("Android Tv");
- imageCardView.setContentText("Android Tv Production Inc.");
- imageCardView.setMainImageDimensions(313, 176);
- }
-
- @Override
- public void onUnbindViewHolder(ViewHolder viewHolder) {
- }
- };
-
- private static final int ACTION_RENT = 2;
- private static final int ACTION_BUY = 3;
-
- protected long mTimeToLoadOverviewRow = 1000;
- protected long mTimeToLoadRelatedRow = 2000;
-
- private Action mActionRent;
- private Action mActionBuy;
-
- protected int mMinVerticalOffset = -100;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setTitle("Leanback Sample App");
-
- mActionRent = new Action(ACTION_RENT, "Rent", "$3.99",
- getResources().getDrawable(R.drawable.ic_action_a));
- mActionBuy = new Action(ACTION_BUY, "Buy $9.99");
-
- ClassPresenterSelector ps = new ClassPresenterSelector();
- FullWidthDetailsOverviewRowPresenter dorPresenter =
- new FullWidthDetailsOverviewRowPresenter(new AbstractDetailsDescriptionPresenter() {
- @Override
- protected void onBindDescription(
- AbstractDetailsDescriptionPresenter.ViewHolder vh, Object item) {
- vh.getTitle().setText("Funny Movie");
- vh.getSubtitle().setText("Android TV Production Inc.");
- vh.getBody().setText("What a great movie!");
- }
- });
-
- ps.addClassPresenter(DetailsOverviewRow.class, dorPresenter);
- ps.addClassPresenter(ListRow.class, new ListRowPresenter());
- mRowsAdapter = new ArrayObjectAdapter(ps);
- }
-
- public void setItem(PhotoItem photoItem) {
- mPhotoItem = photoItem;
- mRowsAdapter.clear();
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null) {
- return;
- }
- Resources res = getActivity().getResources();
- DetailsOverviewRow dor = new DetailsOverviewRow(mPhotoItem.getTitle());
- dor.setImageDrawable(res.getDrawable(mPhotoItem.getImageResourceId()));
- SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();
- adapter.set(ACTION_RENT, mActionRent);
- adapter.set(ACTION_BUY, mActionBuy);
- dor.setActionsAdapter(adapter);
- mRowsAdapter.add(0, dor);
- setSelectedPosition(0, true);
- }
- }, mTimeToLoadOverviewRow);
-
-
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null) {
- return;
- }
- for (int i = 0; i < NUM_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(mCardPresenter);
- listRowAdapter.add(new PhotoItem("Hello world", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("This is a test", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("Android TV", R.drawable.spiderman));
- listRowAdapter.add(new PhotoItem("Leanback", R.drawable.spiderman));
- HeaderItem header = new HeaderItem(i, "Row " + i);
- mRowsAdapter.add(new ListRow(header, listRowAdapter));
- }
- }
- }, mTimeToLoadRelatedRow);
-
- setAdapter(mRowsAdapter);
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
deleted file mode 100644
index fa324bf..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.os.Bundle;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class GuidedStepFragmentTest extends GuidedStepFragmentTestBase {
-
- private static final int ON_DESTROY_TIMEOUT = 5000;
-
- @Test
- public void nextAndBack() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1000) {
- GuidedStepFragment.add(obj.getFragmentManager(),
- new GuidedStepTestFragment(secondFragmentName));
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
-
- GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
- verify(first, times(1)).onCreate(nullable(Bundle.class));
- verify(first, times(1)).onCreateGuidance(nullable(Bundle.class));
- verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
- verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(1)).onViewStateRestored(nullable(Bundle.class));
- verify(first, times(1)).onStart();
- verify(first, times(1)).onResume();
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(first, times(1)).onGuidedActionClicked(any(GuidedAction.class));
-
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(first, times(1)).onPause();
- verify(first, times(1)).onStop();
- verify(first, times(1)).onDestroyView();
- verify(second, times(1)).onCreate(nullable(Bundle.class));
- verify(second, times(1)).onCreateGuidance(nullable(Bundle.class));
- verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
- verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(second, times(1)).onViewStateRestored(nullable(Bundle.class));
- verify(second, times(1)).onStart();
- verify(second, times(1)).onResume();
-
- sendKey(KeyEvent.KEYCODE_BACK);
-
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(second, times(1)).onPause();
- verify(second, times(1)).onStop();
- verify(second, times(1)).onDestroyView();
- verify(second, times(1)).onDestroy();
- verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(2)).onViewStateRestored(nullable(Bundle.class));
- verify(first, times(2)).onStart();
- verify(first, times(2)).onResume();
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- assertTrue(activity.isDestroyed());
- }
-
- @Test
- public void restoreFragments() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
- actions.add(new GuidedAction.Builder().id(1001).editable(true).title("text")
- .build());
- actions.add(new GuidedAction.Builder().id(1002).editable(true).title("text")
- .autoSaveRestoreEnabled(false).build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1000) {
- GuidedStepFragment.add(obj.getFragmentManager(),
- new GuidedStepTestFragment(secondFragmentName));
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
- first.getFragment().findActionById(1001).setTitle("modified text");
- first.getFragment().findActionById(1002).setTitle("modified text");
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new EnterTransitionFinish(second));
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(1)).onDestroy();
- verify(second, times(2)).onCreate(nullable(Bundle.class));
- verify(second, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(second, times(1)).onDestroy();
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(second, times(2)).onPause();
- verify(second, times(2)).onStop();
- verify(second, times(2)).onDestroyView();
- verify(second, times(2)).onDestroy();
- assertEquals("modified text", first.getFragment().findActionById(1001).getTitle());
- assertEquals("text", first.getFragment().findActionById(1002).getTitle());
- verify(first, times(2)).onCreate(nullable(Bundle.class));
- verify(first, times(2)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- }
-
-
- @Test
- public void finishGuidedStepFragment_finishes_activity() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("Finish activity").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1001) {
- obj.getFragment().finishGuidedStepFragments();
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- View viewFinish = first.getFragment().getActionItemView(0);
- assertTrue(viewFinish.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- }
-
- @Test
- public void finishGuidedStepFragment_finishes_fragments() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("Finish fragments").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1001) {
- obj.getFragment().finishGuidedStepFragments();
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName,
- false /*asRoot*/);
-
- View viewFinish = first.getFragment().getActionItemView(0);
- assertTrue(viewFinish.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
-
- // fragment should be destroyed, activity should not destroyed
- waitOnDestroy(first, 1);
- assertFalse(activity.isDestroyed());
- }
-
- @Test
- public void subActions() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- final boolean[] expandSubActionInOnCreateView = new boolean[] {false};
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- if (expandSubActionInOnCreateView[0]) {
- obj.getFragment().expandAction(obj.getFragment().findActionById(1000), false);
- }
- return null;
- }
- }).when(first).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- List<GuidedAction> subActions = new ArrayList<GuidedAction>();
- subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
- subActions.add(new GuidedAction.Builder().id(2001).title("item2").build());
- actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
- .title("list").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) {
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- if (action.getId() == 2000) {
- return true;
- } else if (action.getId() == 2001) {
- GuidedStepFragment.add(obj.getFragmentManager(),
- new GuidedStepTestFragment(secondFragmentName));
- return false;
- }
- return false;
- }
- }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestFragment.Provider second = mockProvider(secondFragmentName);
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- // after clicked, it sub actions list should expand
- View viewForList = first.getFragment().getActionItemView(0);
- assertTrue(viewForList.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(viewForList.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
- assertEquals(2000, actionCapture.getValue().getId());
- // after clicked a sub action, it sub actions list should close
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertTrue(viewForList.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
-
- assertFalse(viewForList.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture2 = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(2)).onSubGuidedActionClicked(actionCapture2.capture());
- assertEquals(2001, actionCapture2.getValue().getId());
-
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
-
- // test expand sub action when return to first fragment
- expandSubActionInOnCreateView[0] = true;
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- assertTrue(first.getFragment().isExpanded());
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(first.getFragment().isExpanded());
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- }
-
- @Test
- public void setActionsWhenSubActionsExpanded() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- List<GuidedAction> subActions = new ArrayList<GuidedAction>();
- subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
- actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
- .title("list").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) {
- GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
- invocation.getMock();
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- if (action.getId() == 2000) {
- List<GuidedAction> newActions = new ArrayList<GuidedAction>();
- newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
- obj.getFragment().setActions(newActions);
- return false;
- }
- return false;
- }
- }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- // after clicked, it sub actions list should expand
- View firstView = first.getFragment().getActionItemView(0);
- assertTrue(firstView.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(firstView.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
- // after clicked a sub action, whole action list is replaced.
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(first.getFragment().isExpanded());
- View newFirstView = first.getFragment().getActionItemView(0);
- assertTrue(newFirstView.hasFocus());
- assertTrue(newFirstView.getVisibility() == View.VISIBLE);
- GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
- .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
- assertEquals(1001, vh.getAction().getId());
-
- }
-
- @Test
- public void buttonActionsRtl() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("action").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
- return null;
- }
- }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
-
- final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName,
- true, View.LAYOUT_DIRECTION_RTL);
-
- assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
- View firstView = first.getFragment().getActionItemView(0);
- assertTrue(firstView.hasFocus());
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
deleted file mode 100644
index 4dcf188..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepFragmentTestActivity extends Activity {
-
- /**
- * Frst Test that will be included in this Activity
- */
- public static final String EXTRA_TEST_NAME = "testName";
- /**
- * True(default) to addAsRoot() for first Test, false to use add()
- */
- public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
-
- /**
- * Layout direction
- */
- public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Intent intent = getIntent();
-
- int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
- if (layoutDirection != -1) {
- findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
- }
- if (savedInstanceState == null) {
- String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
- if (firstTestName != null) {
- GuidedStepTestFragment testFragment = new GuidedStepTestFragment(firstTestName);
- if (intent.getBooleanExtra(EXTRA_ADD_AS_ROOT, true)) {
- GuidedStepTestFragment.addAsRoot(this, testFragment, android.R.id.content);
- } else {
- GuidedStepTestFragment.add(getFragmentManager(), testFragment,
- android.R.id.content);
- }
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
deleted file mode 100644
index 7059c9a..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.view.View;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.rules.TestName;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepFragmentTestBase {
-
- private static final long TIMEOUT = 5000;
-
- @Rule public TestName mUnitTestName = new TestName();
-
- @Rule
- public ActivityTestRule<GuidedStepFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(GuidedStepFragmentTestActivity.class, false, false);
-
- @Before
- public void clearTests() {
- GuidedStepTestFragment.clearTests();
- }
-
- public static class ExpandTransitionFinish extends PollingCheck.PollingCheckCondition {
- GuidedStepTestFragment.Provider mProvider;
-
- public ExpandTransitionFinish(GuidedStepTestFragment.Provider provider) {
- mProvider = provider;
- }
-
- @Override
- public boolean canPreProceed() {
- return false;
- }
-
- @Override
- public boolean canProceed() {
- GuidedStepTestFragment fragment = mProvider.getFragment();
- if (fragment != null && fragment.getView() != null) {
- if (!fragment.getGuidedActionsStylist().isInExpandTransition()) {
- // expand transition finishes
- return true;
- }
- }
- return false;
- }
- }
-
- public static void waitOnDestroy(GuidedStepTestFragment.Provider provider,
- int times) {
- verify(provider, timeout((int)TIMEOUT).times(times)).onDestroy();
- }
-
- public static class EnterTransitionFinish extends PollingCheck.PollingCheckCondition {
- PollingCheck.ViewScreenPositionDetector mDector =
- new PollingCheck.ViewScreenPositionDetector();
-
- GuidedStepTestFragment.Provider mProvider;
-
- public EnterTransitionFinish(GuidedStepTestFragment.Provider provider) {
- mProvider = provider;
- }
- @Override
- public boolean canProceed() {
- GuidedStepTestFragment fragment = mProvider.getFragment();
- if (fragment != null && fragment.getView() != null) {
- View view = fragment.getView().findViewById(R.id.guidance_title);
- if (view != null) {
- if (mDector.isViewStableOnScreen(view)) {
- return true;
- }
- }
- }
- return false;
- }
- }
-
- public static void sendKey(int keyCode) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
- }
-
- public String generateMethodTestName(String testName) {
- return mUnitTestName.getMethodName() + "_" + testName;
- }
-
- public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName,
- boolean addAsRoot) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName,
- boolean addAsRoot, int layoutDirection) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
- intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepTestFragment.Provider mockProvider(String testName) {
- GuidedStepTestFragment.Provider test = mock(GuidedStepTestFragment.Provider.class);
- when(test.getActivity()).thenCallRealMethod();
- when(test.getFragmentManager()).thenCallRealMethod();
- when(test.getFragment()).thenCallRealMethod();
- GuidedStepTestFragment.setupTest(testName, test);
- return test;
- }
-}
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
deleted file mode 100644
index b4d9b59..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
+++ /dev/null
@@ -1,454 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.os.Bundle;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class GuidedStepSupportFragmentTest extends GuidedStepSupportFragmentTestBase {
-
- private static final int ON_DESTROY_TIMEOUT = 5000;
-
- @Test
- public void nextAndBack() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1000) {
- GuidedStepSupportFragment.add(obj.getFragmentManager(),
- new GuidedStepTestSupportFragment(secondFragmentName));
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
-
- GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
- verify(first, times(1)).onCreate(nullable(Bundle.class));
- verify(first, times(1)).onCreateGuidance(nullable(Bundle.class));
- verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
- verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(1)).onViewStateRestored(nullable(Bundle.class));
- verify(first, times(1)).onStart();
- verify(first, times(1)).onResume();
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(first, times(1)).onGuidedActionClicked(any(GuidedAction.class));
-
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(first, times(1)).onPause();
- verify(first, times(1)).onStop();
- verify(first, times(1)).onDestroyView();
- verify(second, times(1)).onCreate(nullable(Bundle.class));
- verify(second, times(1)).onCreateGuidance(nullable(Bundle.class));
- verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
- verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(second, times(1)).onViewStateRestored(nullable(Bundle.class));
- verify(second, times(1)).onStart();
- verify(second, times(1)).onResume();
-
- sendKey(KeyEvent.KEYCODE_BACK);
-
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(second, times(1)).onPause();
- verify(second, times(1)).onStop();
- verify(second, times(1)).onDestroyView();
- verify(second, times(1)).onDestroy();
- verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(2)).onViewStateRestored(nullable(Bundle.class));
- verify(first, times(2)).onStart();
- verify(first, times(2)).onResume();
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- assertTrue(activity.isDestroyed());
- }
-
- @Test
- public void restoreFragments() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("OK").build());
- actions.add(new GuidedAction.Builder().id(1001).editable(true).title("text")
- .build());
- actions.add(new GuidedAction.Builder().id(1002).editable(true).title("text")
- .autoSaveRestoreEnabled(false).build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1000) {
- GuidedStepSupportFragment.add(obj.getFragmentManager(),
- new GuidedStepTestSupportFragment(secondFragmentName));
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
- first.getFragment().findActionById(1001).setTitle("modified text");
- first.getFragment().findActionById(1002).setTitle("modified text");
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new EnterTransitionFinish(second));
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(first, times(1)).onDestroy();
- verify(second, times(2)).onCreate(nullable(Bundle.class));
- verify(second, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- verify(second, times(1)).onDestroy();
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(second, times(2)).onPause();
- verify(second, times(2)).onStop();
- verify(second, times(2)).onDestroyView();
- verify(second, times(2)).onDestroy();
- assertEquals("modified text", first.getFragment().findActionById(1001).getTitle());
- assertEquals("text", first.getFragment().findActionById(1002).getTitle());
- verify(first, times(2)).onCreate(nullable(Bundle.class));
- verify(first, times(2)).onCreateActions(any(List.class), nullable(Bundle.class));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- }
-
-
- @Test
- public void finishGuidedStepSupportFragment_finishes_activity() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("Finish activity").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1001) {
- obj.getFragment().finishGuidedStepSupportFragments();
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- View viewFinish = first.getFragment().getActionItemView(0);
- assertTrue(viewFinish.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- }
-
- @Test
- public void finishGuidedStepSupportFragment_finishes_fragments() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("Finish fragments").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- if (action.getId() == 1001) {
- obj.getFragment().finishGuidedStepSupportFragments();
- }
- return null;
- }
- }).when(first).onGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName,
- false /*asRoot*/);
-
- View viewFinish = first.getFragment().getActionItemView(0);
- assertTrue(viewFinish.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
-
- // fragment should be destroyed, activity should not destroyed
- waitOnDestroy(first, 1);
- assertFalse(activity.isDestroyed());
- }
-
- @Test
- public void subActions() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- final String secondFragmentName = generateMethodTestName("second");
- final boolean[] expandSubActionInOnCreateView = new boolean[] {false};
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- if (expandSubActionInOnCreateView[0]) {
- obj.getFragment().expandAction(obj.getFragment().findActionById(1000), false);
- }
- return null;
- }
- }).when(first).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- List<GuidedAction> subActions = new ArrayList<GuidedAction>();
- subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
- subActions.add(new GuidedAction.Builder().id(2001).title("item2").build());
- actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
- .title("list").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) {
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- if (action.getId() == 2000) {
- return true;
- } else if (action.getId() == 2001) {
- GuidedStepSupportFragment.add(obj.getFragmentManager(),
- new GuidedStepTestSupportFragment(secondFragmentName));
- return false;
- }
- return false;
- }
- }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
-
- GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName);
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- // after clicked, it sub actions list should expand
- View viewForList = first.getFragment().getActionItemView(0);
- assertTrue(viewForList.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(viewForList.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
- assertEquals(2000, actionCapture.getValue().getId());
- // after clicked a sub action, it sub actions list should close
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertTrue(viewForList.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
-
- assertFalse(viewForList.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture2 = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(2)).onSubGuidedActionClicked(actionCapture2.capture());
- assertEquals(2001, actionCapture2.getValue().getId());
-
- PollingCheck.waitFor(new EnterTransitionFinish(second));
- verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
-
- // test expand sub action when return to first fragment
- expandSubActionInOnCreateView[0] = true;
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new EnterTransitionFinish(first));
- verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
- nullable(Bundle.class), any(View.class));
- assertTrue(first.getFragment().isExpanded());
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(first.getFragment().isExpanded());
-
- sendKey(KeyEvent.KEYCODE_BACK);
- PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
- verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
- }
-
- @Test
- public void setActionsWhenSubActionsExpanded() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- List<GuidedAction> subActions = new ArrayList<GuidedAction>();
- subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
- actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
- .title("list").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) {
- GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
- invocation.getMock();
- GuidedAction action = (GuidedAction) invocation.getArguments()[0];
- if (action.getId() == 2000) {
- List<GuidedAction> newActions = new ArrayList<GuidedAction>();
- newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
- obj.getFragment().setActions(newActions);
- return false;
- }
- return false;
- }
- }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
-
- // after clicked, it sub actions list should expand
- View firstView = first.getFragment().getActionItemView(0);
- assertTrue(firstView.hasFocus());
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(firstView.hasFocus());
-
- sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
- ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
- verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
- // after clicked a sub action, whole action list is replaced.
- PollingCheck.waitFor(new ExpandTransitionFinish(first));
- assertFalse(first.getFragment().isExpanded());
- View newFirstView = first.getFragment().getActionItemView(0);
- assertTrue(newFirstView.hasFocus());
- assertTrue(newFirstView.getVisibility() == View.VISIBLE);
- GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
- .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
- assertEquals(1001, vh.getAction().getId());
-
- }
-
- @Test
- public void buttonActionsRtl() throws Throwable {
- final String firstFragmentName = generateMethodTestName("first");
- GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1000).title("action").build());
- return null;
- }
- }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
- doAnswer(new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) {
- List actions = (List) invocation.getArguments()[0];
- actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
- return null;
- }
- }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
-
- final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName,
- true, View.LAYOUT_DIRECTION_RTL);
-
- assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
- View firstView = first.getFragment().getActionItemView(0);
- assertTrue(firstView.hasFocus());
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
deleted file mode 100644
index fb877ed..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFragmentTestActivity.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.support.v4.app.FragmentActivity;
-import android.content.Intent;
-import android.os.Bundle;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepSupportFragmentTestActivity extends FragmentActivity {
-
- /**
- * Frst Test that will be included in this Activity
- */
- public static final String EXTRA_TEST_NAME = "testName";
- /**
- * True(default) to addAsRoot() for first Test, false to use add()
- */
- public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
-
- /**
- * Layout direction
- */
- public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Intent intent = getIntent();
-
- int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
- if (layoutDirection != -1) {
- findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
- }
- if (savedInstanceState == null) {
- String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
- if (firstTestName != null) {
- GuidedStepTestSupportFragment testFragment = new GuidedStepTestSupportFragment(firstTestName);
- if (intent.getBooleanExtra(EXTRA_ADD_AS_ROOT, true)) {
- GuidedStepTestSupportFragment.addAsRoot(this, testFragment, android.R.id.content);
- } else {
- GuidedStepTestSupportFragment.add(getSupportFragmentManager(), testFragment,
- android.R.id.content);
- }
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
deleted file mode 100644
index 17533fa..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
+++ /dev/null
@@ -1,149 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFrgamentTestBase.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.view.View;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.rules.TestName;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepSupportFragmentTestBase {
-
- private static final long TIMEOUT = 5000;
-
- @Rule public TestName mUnitTestName = new TestName();
-
- @Rule
- public ActivityTestRule<GuidedStepSupportFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(GuidedStepSupportFragmentTestActivity.class, false, false);
-
- @Before
- public void clearTests() {
- GuidedStepTestSupportFragment.clearTests();
- }
-
- public static class ExpandTransitionFinish extends PollingCheck.PollingCheckCondition {
- GuidedStepTestSupportFragment.Provider mProvider;
-
- public ExpandTransitionFinish(GuidedStepTestSupportFragment.Provider provider) {
- mProvider = provider;
- }
-
- @Override
- public boolean canPreProceed() {
- return false;
- }
-
- @Override
- public boolean canProceed() {
- GuidedStepTestSupportFragment fragment = mProvider.getFragment();
- if (fragment != null && fragment.getView() != null) {
- if (!fragment.getGuidedActionsStylist().isInExpandTransition()) {
- // expand transition finishes
- return true;
- }
- }
- return false;
- }
- }
-
- public static void waitOnDestroy(GuidedStepTestSupportFragment.Provider provider,
- int times) {
- verify(provider, timeout((int)TIMEOUT).times(times)).onDestroy();
- }
-
- public static class EnterTransitionFinish extends PollingCheck.PollingCheckCondition {
- PollingCheck.ViewScreenPositionDetector mDector =
- new PollingCheck.ViewScreenPositionDetector();
-
- GuidedStepTestSupportFragment.Provider mProvider;
-
- public EnterTransitionFinish(GuidedStepTestSupportFragment.Provider provider) {
- mProvider = provider;
- }
- @Override
- public boolean canProceed() {
- GuidedStepTestSupportFragment fragment = mProvider.getFragment();
- if (fragment != null && fragment.getView() != null) {
- View view = fragment.getView().findViewById(R.id.guidance_title);
- if (view != null) {
- if (mDector.isViewStableOnScreen(view)) {
- return true;
- }
- }
- }
- return false;
- }
- }
-
- public static void sendKey(int keyCode) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
- }
-
- public String generateMethodTestName(String testName) {
- return mUnitTestName.getMethodName() + "_" + testName;
- }
-
- public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName,
- boolean addAsRoot) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName,
- boolean addAsRoot, int layoutDirection) {
- Intent intent = new Intent();
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
- intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
- return activityTestRule.launchActivity(intent);
- }
-
- public GuidedStepTestSupportFragment.Provider mockProvider(String testName) {
- GuidedStepTestSupportFragment.Provider test = mock(GuidedStepTestSupportFragment.Provider.class);
- when(test.getActivity()).thenCallRealMethod();
- when(test.getFragmentManager()).thenCallRealMethod();
- when(test.getFragment()).thenCallRealMethod();
- GuidedStepTestSupportFragment.setupTest(testName, test);
- return test;
- }
-}
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
deleted file mode 100644
index c530925..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.FragmentManager;
-import android.os.Bundle;
-import android.view.ViewGroup;
-import android.view.View;
-import android.view.LayoutInflater;
-
-
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-
-import java.util.List;
-import java.util.HashMap;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepTestFragment extends GuidedStepFragment {
-
- private static final String KEY_TEST_NAME = "key_test_name";
-
- private static final HashMap<String, Provider> sTestMap = new HashMap<String, Provider>();
-
- public static class Provider {
-
- GuidedStepTestFragment mFragment;
-
- public void onCreate(Bundle savedInstanceState) {
- }
-
- public void onSaveInstanceState(Bundle outState) {
- }
-
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- return new Guidance("", "", "", null);
- }
-
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- public void onGuidedActionClicked(GuidedAction action) {
- }
-
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return true;
- }
-
- public void onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState, View result) {
- }
-
- public void onDestroyView() {
- }
-
- public void onDestroy() {
- }
-
- public void onStart() {
- }
-
- public void onStop() {
- }
-
- public void onResume() {
- }
-
- public void onPause() {
- }
-
- public void onViewStateRestored(Bundle bundle) {
- }
-
- public void onDetach() {
- }
-
- public GuidedStepTestFragment getFragment() {
- return mFragment;
- }
-
- public Activity getActivity() {
- return mFragment.getActivity();
- }
-
- public FragmentManager getFragmentManager() {
- return mFragment.getFragmentManager();
- }
- }
-
- public static void setupTest(String testName, Provider provider) {
- sTestMap.put(testName, provider);
- }
-
- public static void clearTests() {
- sTestMap.clear();
- }
-
- CharSequence mTestName;
- Provider mProvider;
-
- public GuidedStepTestFragment() {
- }
-
- public GuidedStepTestFragment(String testName) {
- setTestName(testName);
- }
-
- public void setTestName(CharSequence testName) {
- mTestName = testName;
- }
-
- public CharSequence getTestName() {
- return mTestName;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mTestName = savedInstanceState.getCharSequence(KEY_TEST_NAME, null);
- }
- mProvider = sTestMap.get(mTestName);
- if (mProvider == null) {
- throw new IllegalArgumentException("you must setupTest()");
- }
- mProvider.mFragment = this;
- super.onCreate(savedInstanceState);
- mProvider.onCreate(savedInstanceState);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putCharSequence(KEY_TEST_NAME, mTestName);
- mProvider.onSaveInstanceState(outState);
- }
-
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- Guidance g = mProvider.onCreateGuidance(savedInstanceState);
- if (g == null) {
- g = new Guidance("", "", "", null);
- }
- return g;
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- mProvider.onCreateActions(actions, savedInstanceState);
- }
-
- @Override
- public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- mProvider.onCreateButtonActions(actions, savedInstanceState);
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- mProvider.onGuidedActionClicked(action);
- }
-
- @Override
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return mProvider.onSubGuidedActionClicked(action);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
- View view = super.onCreateView(inflater, container, state);
- mProvider.onCreateView(inflater, container, state, view);
- return view;
- }
-
- @Override
- public void onDestroyView() {
- mProvider.onDestroyView();
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- mProvider.onDestroy();
- super.onDestroy();
- }
-
- @Override
- public void onPause() {
- mProvider.onPause();
- super.onPause();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mProvider.onResume();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mProvider.onStart();
- }
-
- @Override
- public void onStop() {
- mProvider.onStop();
- super.onStop();
- }
-
- @Override
- public void onDetach() {
- mProvider.onDetach();
- super.onDetach();
- }
-
- @Override
- public void onViewStateRestored(Bundle bundle) {
- super.onViewStateRestored(bundle);
- mProvider.onViewStateRestored(bundle);
- }
-}
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
deleted file mode 100644
index bafc2db..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
+++ /dev/null
@@ -1,242 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepTestFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.os.Bundle;
-import android.view.ViewGroup;
-import android.view.View;
-import android.view.LayoutInflater;
-
-
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-
-import java.util.List;
-import java.util.HashMap;
-
-/**
- * @hide from javadoc
- */
-public class GuidedStepTestSupportFragment extends GuidedStepSupportFragment {
-
- private static final String KEY_TEST_NAME = "key_test_name";
-
- private static final HashMap<String, Provider> sTestMap = new HashMap<String, Provider>();
-
- public static class Provider {
-
- GuidedStepTestSupportFragment mFragment;
-
- public void onCreate(Bundle savedInstanceState) {
- }
-
- public void onSaveInstanceState(Bundle outState) {
- }
-
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- return new Guidance("", "", "", null);
- }
-
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- }
-
- public void onGuidedActionClicked(GuidedAction action) {
- }
-
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return true;
- }
-
- public void onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState, View result) {
- }
-
- public void onDestroyView() {
- }
-
- public void onDestroy() {
- }
-
- public void onStart() {
- }
-
- public void onStop() {
- }
-
- public void onResume() {
- }
-
- public void onPause() {
- }
-
- public void onViewStateRestored(Bundle bundle) {
- }
-
- public void onDetach() {
- }
-
- public GuidedStepTestSupportFragment getFragment() {
- return mFragment;
- }
-
- public FragmentActivity getActivity() {
- return mFragment.getActivity();
- }
-
- public FragmentManager getFragmentManager() {
- return mFragment.getFragmentManager();
- }
- }
-
- public static void setupTest(String testName, Provider provider) {
- sTestMap.put(testName, provider);
- }
-
- public static void clearTests() {
- sTestMap.clear();
- }
-
- CharSequence mTestName;
- Provider mProvider;
-
- public GuidedStepTestSupportFragment() {
- }
-
- public GuidedStepTestSupportFragment(String testName) {
- setTestName(testName);
- }
-
- public void setTestName(CharSequence testName) {
- mTestName = testName;
- }
-
- public CharSequence getTestName() {
- return mTestName;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- mTestName = savedInstanceState.getCharSequence(KEY_TEST_NAME, null);
- }
- mProvider = sTestMap.get(mTestName);
- if (mProvider == null) {
- throw new IllegalArgumentException("you must setupTest()");
- }
- mProvider.mFragment = this;
- super.onCreate(savedInstanceState);
- mProvider.onCreate(savedInstanceState);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putCharSequence(KEY_TEST_NAME, mTestName);
- mProvider.onSaveInstanceState(outState);
- }
-
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- Guidance g = mProvider.onCreateGuidance(savedInstanceState);
- if (g == null) {
- g = new Guidance("", "", "", null);
- }
- return g;
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- mProvider.onCreateActions(actions, savedInstanceState);
- }
-
- @Override
- public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- mProvider.onCreateButtonActions(actions, savedInstanceState);
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- mProvider.onGuidedActionClicked(action);
- }
-
- @Override
- public boolean onSubGuidedActionClicked(GuidedAction action) {
- return mProvider.onSubGuidedActionClicked(action);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
- View view = super.onCreateView(inflater, container, state);
- mProvider.onCreateView(inflater, container, state, view);
- return view;
- }
-
- @Override
- public void onDestroyView() {
- mProvider.onDestroyView();
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- mProvider.onDestroy();
- super.onDestroy();
- }
-
- @Override
- public void onPause() {
- mProvider.onPause();
- super.onPause();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mProvider.onResume();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mProvider.onStart();
- }
-
- @Override
- public void onStop() {
- mProvider.onStop();
- super.onStop();
- }
-
- @Override
- public void onDetach() {
- mProvider.onDetach();
- super.onDetach();
- }
-
- @Override
- public void onViewStateRestored(Bundle bundle) {
- super.onViewStateRestored(bundle);
- mProvider.onViewStateRestored(bundle);
- }
-}
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
deleted file mode 100644
index e05237f..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import android.os.Bundle;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.FocusHighlightHelper;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class HeadersFragmentTest extends SingleFragmentTestBase {
-
- static void loadData(ArrayObjectAdapter adapter, int numRows) {
- for (int i = 0; i < numRows; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- public static class F_defaultScale extends HeadersFragment {
- final ListRowPresenter mPresenter = new ListRowPresenter();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
- setAdapter(adapter);
- loadData(adapter, 10);
- }
- }
-
- @Test
- public void defaultScale() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultScale.class, 1000);
-
- final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
- assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
- }
-
- public static class F_disableScale extends HeadersFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
- loadData(adapter, 10);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
- }
- }
-
- @Test
- public void disableScale() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(F_disableScale.class, 1000);
-
- final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
- assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
- }
-
- public static class F_disableScaleInConstructor extends HeadersFragment {
- public F_disableScaleInConstructor() {
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter(), false);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
- loadData(adapter, 10);
- }
- }
-
- @Test
- public void disableScaleInConstructor() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(
- F_disableScaleInConstructor.class, 1000);
-
- final VerticalGridView gridView = ((HeadersFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
- assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
deleted file mode 100644
index 7ec69b9..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from HeadersFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import android.os.Bundle;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.FocusHighlightHelper;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class HeadersSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- static void loadData(ArrayObjectAdapter adapter, int numRows) {
- for (int i = 0; i < numRows; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter();
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- public static class F_defaultScale extends HeadersSupportFragment {
- final ListRowPresenter mPresenter = new ListRowPresenter();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(mPresenter);
- setAdapter(adapter);
- loadData(adapter, 10);
- }
- }
-
- @Test
- public void defaultScale() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultScale.class, 1000);
-
- final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertTrue(vh.itemView.getScaleX() - 1.0f > 0.05f);
- assertTrue(vh.itemView.getScaleY() - 1.0f > 0.05f);
- }
-
- public static class F_disableScale extends HeadersSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
- loadData(adapter, 10);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView(), false);
- }
- }
-
- @Test
- public void disableScale() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_disableScale.class, 1000);
-
- final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
- assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
- }
-
- public static class F_disableScaleInConstructor extends HeadersSupportFragment {
- public F_disableScaleInConstructor() {
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter(), false);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
- setAdapter(adapter);
- loadData(adapter, 10);
- }
- }
-
- @Test
- public void disableScaleInConstructor() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
- F_disableScaleInConstructor.class, 1000);
-
- final VerticalGridView gridView = ((HeadersSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(0);
- assertEquals(vh.itemView.getScaleX(), 1f, 0.001f);
- assertEquals(vh.itemView.getScaleY(), 1f, 0.001f);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
deleted file mode 100644
index 6353ef9..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
+++ /dev/null
@@ -1,371 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static junit.framework.Assert.assertEquals;
-
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.test.filters.FlakyTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.Suppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.media.PlaybackControlGlue;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.PlaybackControlsRow;
-import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class PlaybackFragmentTest extends SingleFragmentTestBase {
-
- private static final String TAG = "PlaybackFragmentTest";
- private static final long TRANSITION_LENGTH = 1000;
-
- @Test
- public void testDetachCalledWhenDestroyFragment() throws Throwable {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestFragment.class, 1000);
- final PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
- PlaybackGlue glue = fragment.getGlue();
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- activity.finish();
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mDestroyCalled;
- }
- });
- assertNull(glue.getHost());
- }
-
- @Test
- public void testSelectedListener() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestFragment.class, 1000);
- PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
-
- assertTrue(fragment.getView().hasFocus());
-
- OnItemViewSelectedListener selectedListener = Mockito.mock(
- OnItemViewSelectedListener.class);
- fragment.setOnItemViewSelectedListener(selectedListener);
-
-
- PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
- SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
- controlsRow.getPrimaryActionsAdapter();
-
- PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
-
- PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
-
- PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
-
- ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
- ArgumentCaptor.forClass(Presenter.ViewHolder.class);
- ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
- ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
- ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
- ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
-
-
- // First navigate left within PlaybackControlsRow items.
- verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
-
- // Now navigate down to a ListRow item.
- ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(fragment.getVerticalGridView());
- verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same list row should be passed to the listener", listRow0,
- rowCaptor.getValue());
- // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
- // selected.
- boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
- || itemCaptor.getValue() == listRow0.getAdapter().get(1));
- assertTrue("None of the items in the first ListRow are passed to the selected listener.",
- listRowItemPassed);
- }
-
- @Test
- public void testClickedListener() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestFragment.class, 1000);
- PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
-
- assertTrue(fragment.getView().hasFocus());
-
- OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
- fragment.setOnItemViewClickedListener(clickedListener);
-
-
- PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
- SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
- controlsRow.getPrimaryActionsAdapter();
-
- PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
-
- PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
-
- PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
-
- ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
- ArgumentCaptor.forClass(Presenter.ViewHolder.class);
- ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
- ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
- ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
- ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
-
-
- // First navigate left within PlaybackControlsRow items.
- verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
-
- // Now navigate down to a ListRow item.
- ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(fragment.getVerticalGridView());
- verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same list row should be passed to the listener", listRow0,
- rowCaptor.getValue());
- boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
- || itemCaptor.getValue() == listRow0.getAdapter().get(1));
- assertTrue("None of the items in the first ListRow are passed to the click listener.",
- listRowItemPassed);
- }
-
- @FlakyTest
- @Suppress
- @Test
- public void alignmentRowToBottom() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestFragment.class, 1000);
- final PlaybackTestFragment fragment = (PlaybackTestFragment) activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() > 2);
-
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- fragment.getVerticalGridView().setSelectedPositionSmooth(
- fragment.getAdapter().size() - 1);
- }
- });
- waitForScrollIdle(fragment.getVerticalGridView());
-
- View lastRow = fragment.getVerticalGridView().getChildAt(
- fragment.getVerticalGridView().getChildCount() - 1);
- assertEquals(fragment.getAdapter().size() - 1,
- fragment.getVerticalGridView().getChildAdapterPosition(lastRow));
- assertTrue(lastRow.hasFocus());
- assertEquals(lastRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - lastRow.getBottom());
- }
-
- public static class PurePlaybackFragment extends PlaybackFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setFadingEnabled(false);
- PlaybackControlsRow row = new PlaybackControlsRow();
- SparseArrayObjectAdapter primaryAdapter = new SparseArrayObjectAdapter(
- new ControlButtonPresenterSelector());
- primaryAdapter.set(0, new PlaybackControlsRow.SkipPreviousAction(getActivity()));
- primaryAdapter.set(1, new PlaybackControlsRow.PlayPauseAction(getActivity()));
- primaryAdapter.set(2, new PlaybackControlsRow.SkipNextAction(getActivity()));
- row.setPrimaryActionsAdapter(primaryAdapter);
- row.setSecondaryActionsAdapter(null);
- setPlaybackRow(row);
- setPlaybackRowPresenter(new PlaybackControlsRowPresenter());
- }
- }
-
- @Test
- public void setupRowAndPresenterWithoutGlue() {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(PurePlaybackFragment.class, 1000);
- final PurePlaybackFragment fragment = (PurePlaybackFragment)
- activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() == 1);
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
- }
-
- public static class ControlGlueFragment extends PlaybackFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- int[] ffspeeds = new int[] {PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0,
- PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1};
- PlaybackGlue glue = new PlaybackControlGlue(
- getActivity(), ffspeeds) {
- @Override
- public boolean hasValidMedia() {
- return true;
- }
-
- @Override
- public boolean isMediaPlaying() {
- return false;
- }
-
- @Override
- public CharSequence getMediaTitle() {
- return "Title";
- }
-
- @Override
- public CharSequence getMediaSubtitle() {
- return "SubTitle";
- }
-
- @Override
- public int getMediaDuration() {
- return 100;
- }
-
- @Override
- public Drawable getMediaArt() {
- return null;
- }
-
- @Override
- public long getSupportedActions() {
- return PlaybackControlGlue.ACTION_PLAY_PAUSE;
- }
-
- @Override
- public int getCurrentSpeedId() {
- return PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
- }
-
- @Override
- public int getCurrentPosition() {
- return 50;
- }
- };
- glue.setHost(new PlaybackFragmentGlueHost(this));
- }
- }
-
- @Test
- public void setupWithControlGlue() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(ControlGlueFragment.class, 1000);
- final ControlGlueFragment fragment = (ControlGlueFragment)
- activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() == 1);
-
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
deleted file mode 100644
index cbc8222..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
+++ /dev/null
@@ -1,374 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from PlaybackFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static junit.framework.Assert.assertEquals;
-
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.test.filters.FlakyTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.Suppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.media.PlaybackControlGlue;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.OnItemViewSelectedListener;
-import android.support.v17.leanback.widget.PlaybackControlsRow;
-import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- private static final String TAG = "PlaybackSupportFragmentTest";
- private static final long TRANSITION_LENGTH = 1000;
-
- @Test
- public void testDetachCalledWhenDestroyFragment() throws Throwable {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
- final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
- PlaybackGlue glue = fragment.getGlue();
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- activity.finish();
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mDestroyCalled;
- }
- });
- assertNull(glue.getHost());
- }
-
- @Test
- public void testSelectedListener() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
- PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
-
- assertTrue(fragment.getView().hasFocus());
-
- OnItemViewSelectedListener selectedListener = Mockito.mock(
- OnItemViewSelectedListener.class);
- fragment.setOnItemViewSelectedListener(selectedListener);
-
-
- PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
- SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
- controlsRow.getPrimaryActionsAdapter();
-
- PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
-
- PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
-
- PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
-
- ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
- ArgumentCaptor.forClass(Presenter.ViewHolder.class);
- ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
- ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
- ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
- ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
-
-
- // First navigate left within PlaybackControlsRow items.
- verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
-
- // Now navigate down to a ListRow item.
- ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(fragment.getVerticalGridView());
- verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same list row should be passed to the listener", listRow0,
- rowCaptor.getValue());
- // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
- // selected.
- boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
- || itemCaptor.getValue() == listRow0.getAdapter().get(1));
- assertTrue("None of the items in the first ListRow are passed to the selected listener.",
- listRowItemPassed);
- }
-
- @Test
- public void testClickedListener() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
- PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
-
- assertTrue(fragment.getView().hasFocus());
-
- OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
- fragment.setOnItemViewClickedListener(clickedListener);
-
-
- PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
- SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
- controlsRow.getPrimaryActionsAdapter();
-
- PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
-
- PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
-
- PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
- primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
-
- ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
- ArgumentCaptor.forClass(Presenter.ViewHolder.class);
- ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
- ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
- ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
- ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
-
-
- // First navigate left within PlaybackControlsRow items.
- verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
-
- sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
- verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same controls row should be passed to the listener", controlsRow,
- rowCaptor.getValue());
- assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
-
- // Now navigate down to a ListRow item.
- ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(fragment.getVerticalGridView());
- verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
- any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
- sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
- verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
- itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
- assertSame("Same list row should be passed to the listener", listRow0,
- rowCaptor.getValue());
- boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
- || itemCaptor.getValue() == listRow0.getAdapter().get(1));
- assertTrue("None of the items in the first ListRow are passed to the click listener.",
- listRowItemPassed);
- }
-
- @FlakyTest
- @Suppress
- @Test
- public void alignmentRowToBottom() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
- final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() > 2);
-
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
-
- activityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- fragment.getVerticalGridView().setSelectedPositionSmooth(
- fragment.getAdapter().size() - 1);
- }
- });
- waitForScrollIdle(fragment.getVerticalGridView());
-
- View lastRow = fragment.getVerticalGridView().getChildAt(
- fragment.getVerticalGridView().getChildCount() - 1);
- assertEquals(fragment.getAdapter().size() - 1,
- fragment.getVerticalGridView().getChildAdapterPosition(lastRow));
- assertTrue(lastRow.hasFocus());
- assertEquals(lastRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - lastRow.getBottom());
- }
-
- public static class PurePlaybackSupportFragment extends PlaybackSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setFadingEnabled(false);
- PlaybackControlsRow row = new PlaybackControlsRow();
- SparseArrayObjectAdapter primaryAdapter = new SparseArrayObjectAdapter(
- new ControlButtonPresenterSelector());
- primaryAdapter.set(0, new PlaybackControlsRow.SkipPreviousAction(getActivity()));
- primaryAdapter.set(1, new PlaybackControlsRow.PlayPauseAction(getActivity()));
- primaryAdapter.set(2, new PlaybackControlsRow.SkipNextAction(getActivity()));
- row.setPrimaryActionsAdapter(primaryAdapter);
- row.setSecondaryActionsAdapter(null);
- setPlaybackRow(row);
- setPlaybackRowPresenter(new PlaybackControlsRowPresenter());
- }
- }
-
- @Test
- public void setupRowAndPresenterWithoutGlue() {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(PurePlaybackSupportFragment.class, 1000);
- final PurePlaybackSupportFragment fragment = (PurePlaybackSupportFragment)
- activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() == 1);
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
- }
-
- public static class ControlGlueFragment extends PlaybackSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- int[] ffspeeds = new int[] {PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0,
- PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1};
- PlaybackGlue glue = new PlaybackControlGlue(
- getActivity(), ffspeeds) {
- @Override
- public boolean hasValidMedia() {
- return true;
- }
-
- @Override
- public boolean isMediaPlaying() {
- return false;
- }
-
- @Override
- public CharSequence getMediaTitle() {
- return "Title";
- }
-
- @Override
- public CharSequence getMediaSubtitle() {
- return "SubTitle";
- }
-
- @Override
- public int getMediaDuration() {
- return 100;
- }
-
- @Override
- public Drawable getMediaArt() {
- return null;
- }
-
- @Override
- public long getSupportedActions() {
- return PlaybackControlGlue.ACTION_PLAY_PAUSE;
- }
-
- @Override
- public int getCurrentSpeedId() {
- return PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
- }
-
- @Override
- public int getCurrentPosition() {
- return 50;
- }
- };
- glue.setHost(new PlaybackSupportFragmentGlueHost(this));
- }
- }
-
- @Test
- public void setupWithControlGlue() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(ControlGlueFragment.class, 1000);
- final ControlGlueFragment fragment = (ControlGlueFragment)
- activity.getTestFragment();
-
- assertTrue(fragment.getAdapter().size() == 1);
-
- View playRow = fragment.getVerticalGridView().getChildAt(0);
- assertTrue(playRow.hasFocus());
- assertEquals(playRow.getResources().getDimensionPixelSize(
- android.support.v17.leanback.test.R.dimen.lb_playback_controls_padding_bottom),
- fragment.getVerticalGridView().getHeight() - playRow.getBottom());
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
deleted file mode 100644
index 027ea02..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.media.PlaybackControlGlue;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.PlaybackControlsRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.Toast;
-
-public class PlaybackTestFragment extends PlaybackFragment {
- private static final String TAG = "PlaybackTestFragment";
-
- /**
- * Change this to choose a different overlay background.
- */
- private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
-
- /**
- * Change this to select hidden
- */
- private static final boolean SECONDARY_HIDDEN = false;
-
- /**
- * Change the number of related content rows.
- */
- private static final int RELATED_CONTENT_ROWS = 3;
-
- private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
- boolean mDestroyCalled;
-
- @Override
- public SparseArrayObjectAdapter getAdapter() {
- return (SparseArrayObjectAdapter) super.getAdapter();
- }
-
- private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.d(TAG, "onItemClicked: " + item + " row " + row);
- }
- };
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mDestroyCalled = true;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Log.i(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- setBackgroundType(BACKGROUND_TYPE);
-
- createComponents(getActivity());
- setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
-
- private void createComponents(Context context) {
- mGlue = new PlaybackControlHelper(context) {
- @Override
- public int getUpdatePeriod() {
- long totalTime = getControlsRow().getDuration();
- if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
- return 1000;
- }
- return 16;
- }
-
- @Override
- public void onActionClicked(Action action) {
- if (action.getId() == R.id.lb_control_picture_in_picture) {
- getActivity().enterPictureInPictureMode();
- return;
- }
- super.onActionClicked(action);
- }
-
- @Override
- protected void onCreateControlsRowAndPresenter() {
- super.onCreateControlsRowAndPresenter();
- getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
- }
- };
-
- mGlue.setHost(new PlaybackFragmentGlueHost(this));
- ClassPresenterSelector selector = new ClassPresenterSelector();
- selector.addClassPresenter(ListRow.class, new ListRowPresenter());
-
- setAdapter(new SparseArrayObjectAdapter(selector));
-
- // Add related content rows
- for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
- listRowAdapter.add("Some related content");
- listRowAdapter.add("Other related content");
- HeaderItem header = new HeaderItem(i, "Row " + i);
- getAdapter().set(1 + i, new ListRow(header, listRowAdapter));
- }
- }
-
- public PlaybackControlGlue getGlue() {
- return mGlue;
- }
-
- abstract static class PlaybackControlHelper extends PlaybackControlGlue {
- /**
- * Change the location of the thumbs up/down controls
- */
- private static final boolean THUMBS_PRIMARY = true;
-
- private static final String FAUX_TITLE = "A short song of silence";
- private static final String FAUX_SUBTITLE = "2014";
- private static final int FAUX_DURATION = 33 * 1000;
-
- // These should match the playback service FF behavior
- private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
-
- private boolean mIsPlaying;
- private int mSpeed = PLAYBACK_SPEED_PAUSED;
- private long mStartTime;
- private long mStartPosition = 0;
-
- private PlaybackControlsRow.RepeatAction mRepeatAction;
- private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
- private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
- private PlaybackControlsRow.PictureInPictureAction mPipAction;
- private static Handler sProgressHandler = new Handler();
-
- private final Runnable mUpdateProgressRunnable = new Runnable() {
- @Override
- public void run() {
- updateProgress();
- sProgressHandler.postDelayed(this, getUpdatePeriod());
- }
- };
-
- PlaybackControlHelper(Context context) {
- super(context, sFastForwardSpeeds);
- mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
- mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
- mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
- mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
- mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
- mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
- }
-
- @Override
- protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
- PresenterSelector presenterSelector) {
- SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
- if (THUMBS_PRIMARY) {
- adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
- adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
- }
- return adapter;
- }
-
- @Override
- public void onActionClicked(Action action) {
- if (shouldDispatchAction(action)) {
- dispatchAction(action);
- return;
- }
- super.onActionClicked(action);
- }
-
- @Override
- public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
- if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
- Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
- if (shouldDispatchAction(action)) {
- dispatchAction(action);
- return true;
- }
- }
- return super.onKey(view, keyCode, keyEvent);
- }
-
- private boolean shouldDispatchAction(Action action) {
- return action == mRepeatAction || action == mThumbsUpAction
- || action == mThumbsDownAction;
- }
-
- private void dispatchAction(Action action) {
- Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
- PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
- multiAction.nextIndex();
- notifyActionChanged(multiAction);
- }
-
- private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
- int index;
- index = getPrimaryActionsAdapter().indexOf(action);
- if (index >= 0) {
- getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
- } else {
- index = getSecondaryActionsAdapter().indexOf(action);
- if (index >= 0) {
- getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
- }
- }
- }
-
- private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
- return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
- }
-
- private ArrayObjectAdapter getSecondaryActionsAdapter() {
- return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
- }
-
- @Override
- public boolean hasValidMedia() {
- return true;
- }
-
- @Override
- public boolean isMediaPlaying() {
- return mIsPlaying;
- }
-
- @Override
- public CharSequence getMediaTitle() {
- return FAUX_TITLE;
- }
-
- @Override
- public CharSequence getMediaSubtitle() {
- return FAUX_SUBTITLE;
- }
-
- @Override
- public int getMediaDuration() {
- return FAUX_DURATION;
- }
-
- @Override
- public Drawable getMediaArt() {
- return null;
- }
-
- @Override
- public long getSupportedActions() {
- return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
- }
-
- @Override
- public int getCurrentSpeedId() {
- return mSpeed;
- }
-
- @Override
- public int getCurrentPosition() {
- int speed;
- if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
- speed = 0;
- } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
- speed = 1;
- } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
- int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
- speed = getFastForwardSpeeds()[index];
- } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
- int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
- speed = -getRewindSpeeds()[index];
- } else {
- return -1;
- }
- long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
- if (position > getMediaDuration()) {
- position = getMediaDuration();
- onPlaybackComplete(true);
- } else if (position < 0) {
- position = 0;
- onPlaybackComplete(false);
- }
- return (int) position;
- }
-
- void onPlaybackComplete(final boolean ended) {
- sProgressHandler.post(new Runnable() {
- @Override
- public void run() {
- if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) {
- pause();
- } else {
- play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
- }
- mStartPosition = 0;
- onStateChanged();
- }
- });
- }
-
- @Override
- public void play(int speed) {
- if (speed == mSpeed) {
- return;
- }
- mStartPosition = getCurrentPosition();
- mSpeed = speed;
- mIsPlaying = true;
- mStartTime = System.currentTimeMillis();
- }
-
- @Override
- public void pause() {
- if (mSpeed == PLAYBACK_SPEED_PAUSED) {
- return;
- }
- mStartPosition = getCurrentPosition();
- mSpeed = PLAYBACK_SPEED_PAUSED;
- mIsPlaying = false;
- }
-
- @Override
- public void next() {
- // Not supported
- }
-
- @Override
- public void previous() {
- // Not supported
- }
-
- @Override
- public void enableProgressUpdating(boolean enable) {
- sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
- if (enable) {
- mUpdateProgressRunnable.run();
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
deleted file mode 100644
index 273df26..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
+++ /dev/null
@@ -1,371 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from PlaybackTestFragment.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v17.leanback.media.PlaybackControlGlue;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.PlaybackControlsRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.Toast;
-
-public class PlaybackTestSupportFragment extends PlaybackSupportFragment {
- private static final String TAG = "PlaybackTestSupportFragment";
-
- /**
- * Change this to choose a different overlay background.
- */
- private static final int BACKGROUND_TYPE = PlaybackSupportFragment.BG_LIGHT;
-
- /**
- * Change this to select hidden
- */
- private static final boolean SECONDARY_HIDDEN = false;
-
- /**
- * Change the number of related content rows.
- */
- private static final int RELATED_CONTENT_ROWS = 3;
-
- private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
- boolean mDestroyCalled;
-
- @Override
- public SparseArrayObjectAdapter getAdapter() {
- return (SparseArrayObjectAdapter) super.getAdapter();
- }
-
- private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- Log.d(TAG, "onItemClicked: " + item + " row " + row);
- }
- };
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mDestroyCalled = true;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Log.i(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- setBackgroundType(BACKGROUND_TYPE);
-
- createComponents(getActivity());
- setOnItemViewClickedListener(mOnItemViewClickedListener);
- }
-
- private void createComponents(Context context) {
- mGlue = new PlaybackControlHelper(context) {
- @Override
- public int getUpdatePeriod() {
- long totalTime = getControlsRow().getDuration();
- if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
- return 1000;
- }
- return 16;
- }
-
- @Override
- public void onActionClicked(Action action) {
- if (action.getId() == R.id.lb_control_picture_in_picture) {
- getActivity().enterPictureInPictureMode();
- return;
- }
- super.onActionClicked(action);
- }
-
- @Override
- protected void onCreateControlsRowAndPresenter() {
- super.onCreateControlsRowAndPresenter();
- getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
- }
- };
-
- mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
- ClassPresenterSelector selector = new ClassPresenterSelector();
- selector.addClassPresenter(ListRow.class, new ListRowPresenter());
-
- setAdapter(new SparseArrayObjectAdapter(selector));
-
- // Add related content rows
- for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
- listRowAdapter.add("Some related content");
- listRowAdapter.add("Other related content");
- HeaderItem header = new HeaderItem(i, "Row " + i);
- getAdapter().set(1 + i, new ListRow(header, listRowAdapter));
- }
- }
-
- public PlaybackControlGlue getGlue() {
- return mGlue;
- }
-
- abstract static class PlaybackControlHelper extends PlaybackControlGlue {
- /**
- * Change the location of the thumbs up/down controls
- */
- private static final boolean THUMBS_PRIMARY = true;
-
- private static final String FAUX_TITLE = "A short song of silence";
- private static final String FAUX_SUBTITLE = "2014";
- private static final int FAUX_DURATION = 33 * 1000;
-
- // These should match the playback service FF behavior
- private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
-
- private boolean mIsPlaying;
- private int mSpeed = PLAYBACK_SPEED_PAUSED;
- private long mStartTime;
- private long mStartPosition = 0;
-
- private PlaybackControlsRow.RepeatAction mRepeatAction;
- private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
- private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
- private PlaybackControlsRow.PictureInPictureAction mPipAction;
- private static Handler sProgressHandler = new Handler();
-
- private final Runnable mUpdateProgressRunnable = new Runnable() {
- @Override
- public void run() {
- updateProgress();
- sProgressHandler.postDelayed(this, getUpdatePeriod());
- }
- };
-
- PlaybackControlHelper(Context context) {
- super(context, sFastForwardSpeeds);
- mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
- mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
- mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
- mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
- mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
- mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
- }
-
- @Override
- protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
- PresenterSelector presenterSelector) {
- SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
- if (THUMBS_PRIMARY) {
- adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
- adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
- }
- return adapter;
- }
-
- @Override
- public void onActionClicked(Action action) {
- if (shouldDispatchAction(action)) {
- dispatchAction(action);
- return;
- }
- super.onActionClicked(action);
- }
-
- @Override
- public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
- if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
- Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
- if (shouldDispatchAction(action)) {
- dispatchAction(action);
- return true;
- }
- }
- return super.onKey(view, keyCode, keyEvent);
- }
-
- private boolean shouldDispatchAction(Action action) {
- return action == mRepeatAction || action == mThumbsUpAction
- || action == mThumbsDownAction;
- }
-
- private void dispatchAction(Action action) {
- Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
- PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
- multiAction.nextIndex();
- notifyActionChanged(multiAction);
- }
-
- private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
- int index;
- index = getPrimaryActionsAdapter().indexOf(action);
- if (index >= 0) {
- getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
- } else {
- index = getSecondaryActionsAdapter().indexOf(action);
- if (index >= 0) {
- getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
- }
- }
- }
-
- private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
- return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
- }
-
- private ArrayObjectAdapter getSecondaryActionsAdapter() {
- return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
- }
-
- @Override
- public boolean hasValidMedia() {
- return true;
- }
-
- @Override
- public boolean isMediaPlaying() {
- return mIsPlaying;
- }
-
- @Override
- public CharSequence getMediaTitle() {
- return FAUX_TITLE;
- }
-
- @Override
- public CharSequence getMediaSubtitle() {
- return FAUX_SUBTITLE;
- }
-
- @Override
- public int getMediaDuration() {
- return FAUX_DURATION;
- }
-
- @Override
- public Drawable getMediaArt() {
- return null;
- }
-
- @Override
- public long getSupportedActions() {
- return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
- }
-
- @Override
- public int getCurrentSpeedId() {
- return mSpeed;
- }
-
- @Override
- public int getCurrentPosition() {
- int speed;
- if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
- speed = 0;
- } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
- speed = 1;
- } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
- int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
- speed = getFastForwardSpeeds()[index];
- } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
- int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
- speed = -getRewindSpeeds()[index];
- } else {
- return -1;
- }
- long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
- if (position > getMediaDuration()) {
- position = getMediaDuration();
- onPlaybackComplete(true);
- } else if (position < 0) {
- position = 0;
- onPlaybackComplete(false);
- }
- return (int) position;
- }
-
- void onPlaybackComplete(final boolean ended) {
- sProgressHandler.post(new Runnable() {
- @Override
- public void run() {
- if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) {
- pause();
- } else {
- play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
- }
- mStartPosition = 0;
- onStateChanged();
- }
- });
- }
-
- @Override
- public void play(int speed) {
- if (speed == mSpeed) {
- return;
- }
- mStartPosition = getCurrentPosition();
- mSpeed = speed;
- mIsPlaying = true;
- mStartTime = System.currentTimeMillis();
- }
-
- @Override
- public void pause() {
- if (mSpeed == PLAYBACK_SPEED_PAUSED) {
- return;
- }
- mStartPosition = getCurrentPosition();
- mSpeed = PLAYBACK_SPEED_PAUSED;
- mIsPlaying = false;
- }
-
- @Override
- public void next() {
- // Not supported
- }
-
- @Override
- public void previous() {
- // Not supported
- }
-
- @Override
- public void enableProgressUpdating(boolean enable) {
- sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
- if (enable) {
- mUpdateProgressRunnable.run();
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
deleted file mode 100644
index 16e37cf..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ /dev/null
@@ -1,464 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SinglePresenterSelector;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class RowsFragmentTest extends SingleFragmentTestBase {
-
- static final StringPresenter sCardPresenter = new StringPresenter();
-
- static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
- for (int i = 0; i < numRows; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
- int index = 0;
- for (int j = 0; j < repeatPerRow; ++j) {
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("This is a test-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("GuidedStepFragment-" + (index++));
- }
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- public static class F_defaultAlignment extends RowsFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }
-
- @Test
- public void defaultAlignment() throws Throwable {
- SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
-
- final Rect rect = new Rect();
-
- final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
- .getVerticalGridView();
- View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
- rect.set(0, 0, row0.getWidth(), row0.getHeight());
- gridView.offsetDescendantRectToMyCoords(row0, rect);
- assertEquals("First row is initially aligned to top of screen", 0, rect.top);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(gridView);
- View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
-
- rect.set(0, 0, row1.getWidth(), row1.getHeight());
- gridView.offsetDescendantRectToMyCoords(row1, rect);
- assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
- }
-
- public static class F_selectBeforeSetAdapter extends RowsFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setSelectedPosition(7, false);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- getVerticalGridView().requestLayout();
- }
- }, 100);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectBeforeSetAdapter() throws InterruptedException {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
-
- final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- public static class F_selectBeforeAddData extends RowsFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- setSelectedPosition(7, false);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- getVerticalGridView().requestLayout();
- }
- }, 100);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- loadData(adapter, 10, 1);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectBeforeAddData() throws InterruptedException {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
-
- final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- public static class F_selectAfterAddData extends RowsFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- setSelectedPosition(7, false);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectAfterAddData() throws InterruptedException {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(F_selectAfterAddData.class, 2000);
-
- final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- static WeakReference<F_restoreSelection> sLastF_restoreSelection;
-
- public static class F_restoreSelection extends RowsFragment {
- public F_restoreSelection() {
- sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- if (savedInstanceState == null) {
- setSelectedPosition(7, false);
- }
- }
- }
-
- @Test
- public void restoreSelection() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(F_restoreSelection.class, 1000);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- }
- );
- SystemClock.sleep(1000);
-
- // mActivity is invalid after recreate(), a new Activity instance is created
- // but we could get Fragment from static variable.
- RowsFragment fragment = sLastF_restoreSelection.get();
- final VerticalGridView gridView = fragment.getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-
- }
-
- public static class F_ListRowWithOnClick extends RowsFragment {
- Presenter.ViewHolder mLastClickedItemViewHolder;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setOnItemViewClickedListener(new OnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- mLastClickedItemViewHolder = itemViewHolder;
- }
- });
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }
-
- @Test
- public void prefetchChildItemsBeforeAttach() throws Throwable {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
-
- F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
- final VerticalGridView gridView = fragment.getVerticalGridView();
- View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
- final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- gridView.setSelectedPositionSmooth(lastRowPos);
- }
- }
- );
- waitForScrollIdle(gridView);
- ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
- RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
- final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
- prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
-
- fragment.mLastClickedItemViewHolder = null;
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- prefetchedListRowVh.getItemViewHolder(0).view.performClick();
- }
- }
- );
- assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
- }
-
- @Test
- public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
- SingleFragmentTestActivity activity =
- launchAndWaitActivity(RowsFragment.class, 2000);
- final RowsFragment fragment = (RowsFragment) activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- ObjectAdapter adapter = new ObjectAdapter() {
- @Override
- public int size() {
- return 0;
- }
-
- @Override
- public Object get(int position) {
- return null;
- }
-
- @Override
- public long getId(int position) {
- return 1;
- }
- };
- adapter.setHasStableIds(true);
- fragment.setAdapter(adapter);
- }
- }
- );
- }
-
- static class StableIdAdapter extends ObjectAdapter {
- ArrayList<Integer> mList = new ArrayList();
-
- @Override
- public long getId(int position) {
- return mList.get(position).longValue();
- }
-
- @Override
- public Object get(int position) {
- return mList.get(position);
- }
-
- @Override
- public int size() {
- return mList.size();
- }
- }
-
- public static class F_rowNotifyItemRangeChange extends BrowseFragment {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- for (int i = 0; i < 2; i++) {
- StableIdAdapter listRowAdapter = new StableIdAdapter();
- listRowAdapter.setHasStableIds(true);
- listRowAdapter.setPresenterSelector(
- new SinglePresenterSelector(sCardPresenter));
- int index = 0;
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- setAdapter(adapter);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- StableIdAdapter rowAdapter = (StableIdAdapter)
- ((ListRow) adapter.get(1)).getAdapter();
- rowAdapter.notifyItemRangeChanged(0, 3);
- }
- }, 500);
- }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- public void rowNotifyItemRangeChange() throws InterruptedException {
- SingleFragmentTestActivity activity = launchAndWaitActivity(
- RowsFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
-
- VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
- .getRowsFragment().getVerticalGridView();
- for (int i = 0; i < verticalGridView.getChildCount(); i++) {
- HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
- .findViewById(R.id.row_content);
- for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
- assertEquals(horizontalGridView.getPaddingTop(),
- horizontalGridView.getChildAt(j).getTop());
- }
- }
- }
-
- public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseFragment {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- prepareEntranceTransition();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- for (int i = 0; i < 2; i++) {
- StableIdAdapter listRowAdapter = new StableIdAdapter();
- listRowAdapter.setHasStableIds(true);
- listRowAdapter.setPresenterSelector(
- new SinglePresenterSelector(sCardPresenter));
- int index = 0;
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- setAdapter(adapter);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- StableIdAdapter rowAdapter = (StableIdAdapter)
- ((ListRow) adapter.get(1)).getAdapter();
- rowAdapter.notifyItemRangeChanged(0, 3);
- }
- }, 500);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- startEntranceTransition();
- }
- }, 520);
- }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
- SingleFragmentTestActivity activity = launchAndWaitActivity(
- RowsFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
-
- VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
- .getRowsFragment().getVerticalGridView();
- for (int i = 0; i < verticalGridView.getChildCount(); i++) {
- HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
- .findViewById(R.id.row_content);
- for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
- assertEquals(horizontalGridView.getPaddingTop(),
- horizontalGridView.getChildAt(j).getTop());
- assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
deleted file mode 100644
index c461f45..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ /dev/null
@@ -1,467 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from RowsFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SinglePresenterSelector;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class RowsSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- static final StringPresenter sCardPresenter = new StringPresenter();
-
- static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
- for (int i = 0; i < numRows; ++i) {
- ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
- int index = 0;
- for (int j = 0; j < repeatPerRow; ++j) {
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("This is a test-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("Hello world-" + (index++));
- listRowAdapter.add("Android TV-" + (index++));
- listRowAdapter.add("Leanback-" + (index++));
- listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
- }
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- }
-
- public static class F_defaultAlignment extends RowsSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }
-
- @Test
- public void defaultAlignment() throws Throwable {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
-
- final Rect rect = new Rect();
-
- final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
- rect.set(0, 0, row0.getWidth(), row0.getHeight());
- gridView.offsetDescendantRectToMyCoords(row0, rect);
- assertEquals("First row is initially aligned to top of screen", 0, rect.top);
-
- sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(gridView);
- View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
- PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
-
- rect.set(0, 0, row1.getWidth(), row1.getHeight());
- gridView.offsetDescendantRectToMyCoords(row1, rect);
- assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
- }
-
- public static class F_selectBeforeSetAdapter extends RowsSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setSelectedPosition(7, false);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- getVerticalGridView().requestLayout();
- }
- }, 100);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectBeforeSetAdapter() throws InterruptedException {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
-
- final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- public static class F_selectBeforeAddData extends RowsSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- setSelectedPosition(7, false);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- getVerticalGridView().requestLayout();
- }
- }, 100);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- loadData(adapter, 10, 1);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectBeforeAddData() throws InterruptedException {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
-
- final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- public static class F_selectAfterAddData extends RowsSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- setSelectedPosition(7, false);
- }
- }, 1000);
- }
- }
-
- @Test
- public void selectAfterAddData() throws InterruptedException {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(F_selectAfterAddData.class, 2000);
-
- final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
- .getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
- }
-
- static WeakReference<F_restoreSelection> sLastF_restoreSelection;
-
- public static class F_restoreSelection extends RowsSupportFragment {
- public F_restoreSelection() {
- sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- if (savedInstanceState == null) {
- setSelectedPosition(7, false);
- }
- }
- }
-
- @Test
- public void restoreSelection() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(F_restoreSelection.class, 1000);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- }
- );
- SystemClock.sleep(1000);
-
- // mActivity is invalid after recreate(), a new Activity instance is created
- // but we could get Fragment from static variable.
- RowsSupportFragment fragment = sLastF_restoreSelection.get();
- final VerticalGridView gridView = fragment.getVerticalGridView();
- assertEquals(7, gridView.getSelectedPosition());
- assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-
- }
-
- public static class F_ListRowWithOnClick extends RowsSupportFragment {
- Presenter.ViewHolder mLastClickedItemViewHolder;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setOnItemViewClickedListener(new OnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
- RowPresenter.ViewHolder rowViewHolder, Row row) {
- mLastClickedItemViewHolder = itemViewHolder;
- }
- });
- ListRowPresenter lrp = new ListRowPresenter();
- ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- setAdapter(adapter);
- loadData(adapter, 10, 1);
- }
- }
-
- @Test
- public void prefetchChildItemsBeforeAttach() throws Throwable {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
-
- F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
- final VerticalGridView gridView = fragment.getVerticalGridView();
- View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
- final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- gridView.setSelectedPositionSmooth(lastRowPos);
- }
- }
- );
- waitForScrollIdle(gridView);
- ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
- gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
- RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
- final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
- prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
-
- fragment.mLastClickedItemViewHolder = null;
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- prefetchedListRowVh.getItemViewHolder(0).view.performClick();
- }
- }
- );
- assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
- }
-
- @Test
- public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
- SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(RowsSupportFragment.class, 2000);
- final RowsSupportFragment fragment = (RowsSupportFragment) activity.getTestFragment();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- new Runnable() {
- public void run() {
- ObjectAdapter adapter = new ObjectAdapter() {
- @Override
- public int size() {
- return 0;
- }
-
- @Override
- public Object get(int position) {
- return null;
- }
-
- @Override
- public long getId(int position) {
- return 1;
- }
- };
- adapter.setHasStableIds(true);
- fragment.setAdapter(adapter);
- }
- }
- );
- }
-
- static class StableIdAdapter extends ObjectAdapter {
- ArrayList<Integer> mList = new ArrayList();
-
- @Override
- public long getId(int position) {
- return mList.get(position).longValue();
- }
-
- @Override
- public Object get(int position) {
- return mList.get(position);
- }
-
- @Override
- public int size() {
- return mList.size();
- }
- }
-
- public static class F_rowNotifyItemRangeChange extends BrowseSupportFragment {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- for (int i = 0; i < 2; i++) {
- StableIdAdapter listRowAdapter = new StableIdAdapter();
- listRowAdapter.setHasStableIds(true);
- listRowAdapter.setPresenterSelector(
- new SinglePresenterSelector(sCardPresenter));
- int index = 0;
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- setAdapter(adapter);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- StableIdAdapter rowAdapter = (StableIdAdapter)
- ((ListRow) adapter.get(1)).getAdapter();
- rowAdapter.notifyItemRangeChanged(0, 3);
- }
- }, 500);
- }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- public void rowNotifyItemRangeChange() throws InterruptedException {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
- RowsSupportFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
-
- VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
- .getRowsSupportFragment().getVerticalGridView();
- for (int i = 0; i < verticalGridView.getChildCount(); i++) {
- HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
- .findViewById(R.id.row_content);
- for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
- assertEquals(horizontalGridView.getPaddingTop(),
- horizontalGridView.getChildAt(j).getTop());
- }
- }
- }
-
- public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseSupportFragment {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- ListRowPresenter lrp = new ListRowPresenter();
- prepareEntranceTransition();
- final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
- for (int i = 0; i < 2; i++) {
- StableIdAdapter listRowAdapter = new StableIdAdapter();
- listRowAdapter.setHasStableIds(true);
- listRowAdapter.setPresenterSelector(
- new SinglePresenterSelector(sCardPresenter));
- int index = 0;
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- listRowAdapter.mList.add(index++);
- HeaderItem header = new HeaderItem(i, "Row " + i);
- adapter.add(new ListRow(header, listRowAdapter));
- }
- setAdapter(adapter);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- StableIdAdapter rowAdapter = (StableIdAdapter)
- ((ListRow) adapter.get(1)).getAdapter();
- rowAdapter.notifyItemRangeChanged(0, 3);
- }
- }, 500);
- new Handler().postDelayed(new Runnable() {
- @Override
- public void run() {
- startEntranceTransition();
- }
- }, 520);
- }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
- RowsSupportFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
-
- VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
- .getRowsSupportFragment().getVerticalGridView();
- for (int i = 0; i < verticalGridView.getChildCount(); i++) {
- HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
- .findViewById(R.id.row_content);
- for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
- assertEquals(horizontalGridView.getPaddingTop(),
- horizontalGridView.getChildAt(j).getTop());
- assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
- }
- }
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
deleted file mode 100644
index 6047a1e..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-import android.util.Log;
-
-public class SingleFragmentTestActivity extends Activity {
-
- /**
- * Fragment that will be added to activity
- */
- public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
-
- public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
-
- public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
- private static final String TAG = "TestActivity";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.d(TAG, "onCreate " + this);
- Intent intent = getIntent();
-
- final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
- if (uiOptions != 0) {
- getWindow().getDecorView().setSystemUiVisibility(uiOptions);
- }
-
- setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
- if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
- try {
- Fragment fragment = (Fragment) Class.forName(
- intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
- FragmentTransaction ft = getFragmentManager().beginTransaction();
- ft.replace(R.id.main_frame, fragment);
- ft.commit();
- } catch (Exception ex) {
- ex.printStackTrace();
- finish();
- }
- }
- }
-
- public Fragment getTestFragment() {
- return getFragmentManager().findFragmentById(R.id.main_frame);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
deleted file mode 100644
index b26d92d..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.Intent;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v7.widget.RecyclerView;
-
-import org.junit.Rule;
-import org.junit.rules.TestName;
-
-public class SingleFragmentTestBase {
-
- private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
-
- @Rule
- public TestName mUnitTestName = new TestName();
-
- @Rule
- public ActivityTestRule<SingleFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(SingleFragmentTestActivity.class, false, false);
-
- public void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
- /**
- * Options that will be passed throught Intent to SingleFragmentTestActivity
- */
- public static class Options {
- int mActivityLayoutId;
- int mUiVisibility;
-
- public Options() {
- }
-
- public Options activityLayoutId(int activityLayoutId) {
- mActivityLayoutId = activityLayoutId;
- return this;
- }
-
- public Options uiVisibility(int uiVisibility) {
- mUiVisibility = uiVisibility;
- return this;
- }
-
- public void collect(Intent intent) {
- if (mActivityLayoutId != 0) {
- intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
- mActivityLayoutId);
- }
- if (mUiVisibility != 0) {
- intent.putExtra(SingleFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
- }
- }
- }
-
- public SingleFragmentTestActivity launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
- return launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
- }
-
- public SingleFragmentTestActivity launchAndWaitActivity(Class fragmentClass, Options options,
- long waitTimeMs) {
- return launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
- }
-
- public SingleFragmentTestActivity launchAndWaitActivity(String firstFragmentName,
- Options options, long waitTimeMs) {
- Intent intent = new Intent();
- intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
- if (options != null) {
- options.collect(intent);
- }
- SingleFragmentTestActivity activity = activityTestRule.launchActivity(intent);
- SystemClock.sleep(waitTimeMs);
- return activity;
- }
-
- protected void waitForScrollIdle(RecyclerView recyclerView) throws Throwable {
- waitForScrollIdle(recyclerView, null);
- }
-
- protected void waitForScrollIdle(RecyclerView recyclerView, Runnable verify) throws Throwable {
- Thread.sleep(100);
- int total = 0;
- while (recyclerView.getLayoutManager().isSmoothScrolling()
- || recyclerView.getScrollState() != recyclerView.SCROLL_STATE_IDLE) {
- if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
- throw new RuntimeException("waitForScrollIdle Timeout");
- }
- try {
- Thread.sleep(100);
- } catch (InterruptedException ex) {
- break;
- }
- if (verify != null) {
- activityTestRule.runOnUiThread(verify);
- }
- }
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
deleted file mode 100644
index 0fc3183..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from SingleFragmentTestActivity.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-import android.util.Log;
-
-public class SingleSupportFragmentTestActivity extends FragmentActivity {
-
- /**
- * Fragment that will be added to activity
- */
- public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
-
- public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
-
- public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
- private static final String TAG = "TestActivity";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.d(TAG, "onCreate " + this);
- Intent intent = getIntent();
-
- final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
- if (uiOptions != 0) {
- getWindow().getDecorView().setSystemUiVisibility(uiOptions);
- }
-
- setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
- if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
- try {
- Fragment fragment = (Fragment) Class.forName(
- intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- ft.replace(R.id.main_frame, fragment);
- ft.commit();
- } catch (Exception ex) {
- ex.printStackTrace();
- finish();
- }
- }
- }
-
- public Fragment getTestFragment() {
- return getSupportFragmentManager().findFragmentById(R.id.main_frame);
- }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
deleted file mode 100644
index 6c00923..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from SingleFrgamentTestBase.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.content.Intent;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v7.widget.RecyclerView;
-
-import org.junit.Rule;
-import org.junit.rules.TestName;
-
-public class SingleSupportFragmentTestBase {
-
- private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
-
- @Rule
- public TestName mUnitTestName = new TestName();
-
- @Rule
- public ActivityTestRule<SingleSupportFragmentTestActivity> activityTestRule =
- new ActivityTestRule<>(SingleSupportFragmentTestActivity.class, false, false);
-
- public void sendKeys(int ...keys) {
- for (int i = 0; i < keys.length; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
- }
- }
-
- /**
- * Options that will be passed throught Intent to SingleSupportFragmentTestActivity
- */
- public static class Options {
- int mActivityLayoutId;
- int mUiVisibility;
-
- public Options() {
- }
-
- public Options activityLayoutId(int activityLayoutId) {
- mActivityLayoutId = activityLayoutId;
- return this;
- }
-
- public Options uiVisibility(int uiVisibility) {
- mUiVisibility = uiVisibility;
- return this;
- }
-
- public void collect(Intent intent) {
- if (mActivityLayoutId != 0) {
- intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
- mActivityLayoutId);
- }
- if (mUiVisibility != 0) {
- intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
- }
- }
- }
-
- public SingleSupportFragmentTestActivity launchAndWaitActivity(Class fragmentClass, long waitTimeMs) {
- return launchAndWaitActivity(fragmentClass.getName(), null, waitTimeMs);
- }
-
- public SingleSupportFragmentTestActivity launchAndWaitActivity(Class fragmentClass, Options options,
- long waitTimeMs) {
- return launchAndWaitActivity(fragmentClass.getName(), options, waitTimeMs);
- }
-
- public SingleSupportFragmentTestActivity launchAndWaitActivity(String firstFragmentName,
- Options options, long waitTimeMs) {
- Intent intent = new Intent();
- intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
- if (options != null) {
- options.collect(intent);
- }
- SingleSupportFragmentTestActivity activity = activityTestRule.launchActivity(intent);
- SystemClock.sleep(waitTimeMs);
- return activity;
- }
-
- protected void waitForScrollIdle(RecyclerView recyclerView) throws Throwable {
- waitForScrollIdle(recyclerView, null);
- }
-
- protected void waitForScrollIdle(RecyclerView recyclerView, Runnable verify) throws Throwable {
- Thread.sleep(100);
- int total = 0;
- while (recyclerView.getLayoutManager().isSmoothScrolling()
- || recyclerView.getScrollState() != recyclerView.SCROLL_STATE_IDLE) {
- if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
- throw new RuntimeException("waitForScrollIdle Timeout");
- }
- try {
- Thread.sleep(100);
- } catch (InterruptedException ex) {
- break;
- }
- if (verify != null) {
- activityTestRule.runOnUiThread(verify);
- }
- }
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
deleted file mode 100644
index 2c36cda..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.app.Fragment;
-import android.os.Bundle;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.VerticalGridPresenter;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class VerticalGridFragmentTest extends SingleFragmentTestBase {
-
- public static class GridFragment extends VerticalGridFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState == null) {
- prepareEntranceTransition();
- }
- VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
- gridPresenter.setNumberOfColumns(3);
- setGridPresenter(gridPresenter);
- setAdapter(new ArrayObjectAdapter());
- }
- }
-
- @Test
- public void immediateRemoveFragment() throws Throwable {
- final SingleFragmentTestActivity activity = launchAndWaitActivity(GridFragment.class, 500);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- GridFragment f = new GridFragment();
- activity.getFragmentManager().beginTransaction()
- .replace(android.R.id.content, f, null).commit();
- f.startEntranceTransition();
- activity.getFragmentManager().beginTransaction()
- .replace(android.R.id.content, new Fragment(), null).commit();
- }
- });
-
- Thread.sleep(1000);
- activity.finish();
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
deleted file mode 100644
index 9ca930a..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VerticalGridFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v17.leanback.app;
-
-import android.support.v4.app.Fragment;
-import android.os.Bundle;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.VerticalGridPresenter;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- public static class GridFragment extends VerticalGridSupportFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState == null) {
- prepareEntranceTransition();
- }
- VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
- gridPresenter.setNumberOfColumns(3);
- setGridPresenter(gridPresenter);
- setAdapter(new ArrayObjectAdapter());
- }
- }
-
- @Test
- public void immediateRemoveFragment() throws Throwable {
- final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(GridFragment.class, 500);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- GridFragment f = new GridFragment();
- activity.getSupportFragmentManager().beginTransaction()
- .replace(android.R.id.content, f, null).commit();
- f.startEntranceTransition();
- activity.getSupportFragmentManager().beginTransaction()
- .replace(android.R.id.content, new Fragment(), null).commit();
- }
- });
-
- Thread.sleep(1000);
- activity.finish();
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
deleted file mode 100644
index 7fe3902..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.media.MediaPlayerGlue;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.ViewGroup;
-
-import junit.framework.Assert;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class VideoFragmentTest extends SingleFragmentTestBase {
-
- public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoFragment {
- boolean mSurfaceCreated;
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
- setSurfaceHolderCallback(new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- mSurfaceCreated = true;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- mSurfaceCreated = false;
- }
- });
-
- return super.onCreateView(inflater, container, savedInstanceState);
- }
- }
-
- @Test
- public void setSurfaceViewCallbackBeforeCreate() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
- Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
- (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
- assertNotNull(fragment1);
- assertTrue(fragment1.mSurfaceCreated);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- activity.getFragmentManager().beginTransaction()
- .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
- .commitAllowingStateLoss();
- }
- });
- SystemClock.sleep(500);
-
- assertFalse(fragment1.mSurfaceCreated);
-
- Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
- (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
- assertNotNull(fragment2);
- assertTrue(fragment2.mSurfaceCreated);
- assertNotSame(fragment1, fragment2);
- }
-
- @Test
- public void setSurfaceViewCallbackAfterCreate() {
- SingleFragmentTestActivity activity = launchAndWaitActivity(VideoFragment.class, 1000);
- VideoFragment fragment = (VideoFragment) activity.getTestFragment();
-
- assertNotNull(fragment);
-
- final boolean[] surfaceCreated = new boolean[1];
- fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- surfaceCreated[0] = true;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- surfaceCreated[0] = false;
- }
- });
- assertTrue(surfaceCreated[0]);
- }
-
- public static class Fragment_withVideoPlayer extends VideoFragment {
- MediaPlayerGlue mGlue;
- int mOnCreateCalled;
- int mOnCreateViewCalled;
- int mOnDestroyViewCalled;
- int mOnDestroyCalled;
- int mGlueAttachedToHost;
- int mGlueDetachedFromHost;
- int mGlueOnReadyForPlaybackCalled;
-
- public Fragment_withVideoPlayer() {
- setRetainInstance(true);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- mOnCreateCalled++;
- super.onCreate(savedInstanceState);
- mGlue = new MediaPlayerGlue(getActivity()) {
- @Override
- protected void onDetachedFromHost() {
- mGlueDetachedFromHost++;
- super.onDetachedFromHost();
- }
-
- @Override
- protected void onAttachedToHost(PlaybackGlueHost host) {
- super.onAttachedToHost(host);
- mGlueAttachedToHost++;
- }
- };
- mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mGlue.setArtist("Leanback");
- mGlue.setTitle("Leanback team at work");
- mGlue.setMediaSource(
- Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
- mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
- @Override
- public void onPreparedStateChanged(PlaybackGlue glue) {
- if (glue.isPrepared()) {
- mGlueOnReadyForPlaybackCalled++;
- mGlue.play();
- }
- }
- });
- mGlue.setHost(new VideoFragmentGlueHost(this));
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mOnCreateViewCalled++;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onDestroyView() {
- mOnDestroyViewCalled++;
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- mOnDestroyCalled++;
- super.onDestroy();
- }
- }
-
- @Test
- public void mediaPlayerGlueInVideoFragment() {
- final SingleFragmentTestActivity activity =
- launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
- final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
- activity.getTestFragment();
-
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mGlue.isMediaPlaying();
- }
- });
-
- assertEquals(1, fragment.mOnCreateCalled);
- assertEquals(1, fragment.mOnCreateViewCalled);
- assertEquals(0, fragment.mOnDestroyViewCalled);
- assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
- View fragmentViewBeforeRecreate = fragment.getView();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
-
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
- }
- });
- View fragmentViewAfterRecreate = fragment.getView();
-
- Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
- assertEquals(1, fragment.mOnCreateCalled);
- assertEquals(2, fragment.mOnCreateViewCalled);
- assertEquals(1, fragment.mOnDestroyViewCalled);
-
- assertEquals(1, fragment.mGlueAttachedToHost);
- assertEquals(0, fragment.mGlueDetachedFromHost);
- assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
-
- activity.finish();
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mGlueDetachedFromHost == 1;
- }
- });
- assertEquals(2, fragment.mOnDestroyViewCalled);
- assertEquals(1, fragment.mOnDestroyCalled);
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
deleted file mode 100644
index d96dc4d..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
+++ /dev/null
@@ -1,256 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VideoFragmentTest.java. DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.media.MediaPlayerGlue;
-import android.support.v17.leanback.media.PlaybackGlue;
-import android.support.v17.leanback.media.PlaybackGlueHost;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.ViewGroup;
-
-import junit.framework.Assert;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class VideoSupportFragmentTest extends SingleSupportFragmentTestBase {
-
- public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoSupportFragment {
- boolean mSurfaceCreated;
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
- setSurfaceHolderCallback(new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- mSurfaceCreated = true;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- mSurfaceCreated = false;
- }
- });
-
- return super.onCreateView(inflater, container, savedInstanceState);
- }
- }
-
- @Test
- public void setSurfaceViewCallbackBeforeCreate() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
- Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
- (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
- assertNotNull(fragment1);
- assertTrue(fragment1.mSurfaceCreated);
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- activity.getSupportFragmentManager().beginTransaction()
- .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
- .commitAllowingStateLoss();
- }
- });
- SystemClock.sleep(500);
-
- assertFalse(fragment1.mSurfaceCreated);
-
- Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
- (Fragment_setSurfaceViewCallbackBeforeCreate) activity.getTestFragment();
- assertNotNull(fragment2);
- assertTrue(fragment2.mSurfaceCreated);
- assertNotSame(fragment1, fragment2);
- }
-
- @Test
- public void setSurfaceViewCallbackAfterCreate() {
- SingleSupportFragmentTestActivity activity = launchAndWaitActivity(VideoSupportFragment.class, 1000);
- VideoSupportFragment fragment = (VideoSupportFragment) activity.getTestFragment();
-
- assertNotNull(fragment);
-
- final boolean[] surfaceCreated = new boolean[1];
- fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- surfaceCreated[0] = true;
- }
-
- @Override
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- surfaceCreated[0] = false;
- }
- });
- assertTrue(surfaceCreated[0]);
- }
-
- public static class Fragment_withVideoPlayer extends VideoSupportFragment {
- MediaPlayerGlue mGlue;
- int mOnCreateCalled;
- int mOnCreateViewCalled;
- int mOnDestroyViewCalled;
- int mOnDestroyCalled;
- int mGlueAttachedToHost;
- int mGlueDetachedFromHost;
- int mGlueOnReadyForPlaybackCalled;
-
- public Fragment_withVideoPlayer() {
- setRetainInstance(true);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- mOnCreateCalled++;
- super.onCreate(savedInstanceState);
- mGlue = new MediaPlayerGlue(getActivity()) {
- @Override
- protected void onDetachedFromHost() {
- mGlueDetachedFromHost++;
- super.onDetachedFromHost();
- }
-
- @Override
- protected void onAttachedToHost(PlaybackGlueHost host) {
- super.onAttachedToHost(host);
- mGlueAttachedToHost++;
- }
- };
- mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mGlue.setArtist("Leanback");
- mGlue.setTitle("Leanback team at work");
- mGlue.setMediaSource(
- Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
- mGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() {
- @Override
- public void onPreparedStateChanged(PlaybackGlue glue) {
- if (glue.isPrepared()) {
- mGlueOnReadyForPlaybackCalled++;
- mGlue.play();
- }
- }
- });
- mGlue.setHost(new VideoSupportFragmentGlueHost(this));
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mOnCreateViewCalled++;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onDestroyView() {
- mOnDestroyViewCalled++;
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- mOnDestroyCalled++;
- super.onDestroy();
- }
- }
-
- @Test
- public void mediaPlayerGlueInVideoSupportFragment() {
- final SingleSupportFragmentTestActivity activity =
- launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
- final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
- activity.getTestFragment();
-
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mGlue.isMediaPlaying();
- }
- });
-
- assertEquals(1, fragment.mOnCreateCalled);
- assertEquals(1, fragment.mOnCreateViewCalled);
- assertEquals(0, fragment.mOnDestroyViewCalled);
- assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
- View fragmentViewBeforeRecreate = fragment.getView();
-
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- activity.recreate();
- }
- });
-
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
- }
- });
- View fragmentViewAfterRecreate = fragment.getView();
-
- Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
- assertEquals(1, fragment.mOnCreateCalled);
- assertEquals(2, fragment.mOnCreateViewCalled);
- assertEquals(1, fragment.mOnDestroyViewCalled);
-
- assertEquals(1, fragment.mGlueAttachedToHost);
- assertEquals(0, fragment.mGlueDetachedFromHost);
- assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
-
- activity.finish();
- PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return fragment.mGlueDetachedFromHost == 1;
- }
- });
- assertEquals(2, fragment.mOnDestroyViewCalled);
- assertEquals(1, fragment.mOnDestroyCalled);
- }
-
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
deleted file mode 100644
index 5de0aa7..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ /dev/null
@@ -1,5631 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.widget;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
-import android.content.Intent;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
-import android.os.Parcelable;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.test.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.widget.DefaultItemAnimator;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
-import android.text.Selection;
-import android.text.Spannable;
-import android.util.DisplayMetrics;
-import android.util.SparseArray;
-import android.util.SparseIntArray;
-import android.util.TypedValue;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class GridWidgetTest {
-
- private static final float DELTA = 1f;
- private static final boolean HUMAN_DELAY = false;
- private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
- private static final int WAIT_FOR_LAYOUT_PASS_TIMEOUT_MS = 2000;
- private static final int WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS = 6000;
-
- protected ActivityTestRule<GridActivity> mActivityTestRule;
- protected GridActivity mActivity;
- protected BaseGridView mGridView;
- protected GridLayoutManager mLayoutManager;
- private GridLayoutManager.OnLayoutCompleteListener mWaitLayoutListener;
- protected int mOrientation;
- protected int mNumRows;
- protected int[] mRemovedItems;
-
- private final Comparator<View> mRowSortComparator = new Comparator<View>() {
- @Override
- public int compare(View lhs, View rhs) {
- if (mOrientation == BaseGridView.HORIZONTAL) {
- return lhs.getLeft() - rhs.getLeft();
- } else {
- return lhs.getTop() - rhs.getTop();
- }
- };
- };
-
- /**
- * Verify margins between items on same row are same.
- */
- private final Runnable mVerifyLayout = new Runnable() {
- @Override
- public void run() {
- verifyMargin();
- }
- };
-
- @Rule public TestName testName = new TestName();
-
- public static void sendKey(int keyCode) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
- }
-
- public static void sendRepeatedKeys(int repeats, int keyCode) {
- for (int i = 0; i < repeats; i++) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
- }
- }
-
- private void humanDelay(int delay) throws InterruptedException {
- if (HUMAN_DELAY) Thread.sleep(delay);
- }
- /**
- * Change size of the Adapter and notifyDataSetChanged.
- */
- private void changeArraySize(final int size) throws Throwable {
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.changeArraySize(size);
- }
- });
- }
-
- static String dumpGridView(BaseGridView gridView) {
- return "findFocus:" + gridView.getRootView().findFocus()
- + " isLayoutRequested:" + gridView.isLayoutRequested()
- + " selectedPosition:" + gridView.getSelectedPosition()
- + " adapter.itemCount:" + gridView.getAdapter().getItemCount()
- + " itemAnimator.isRunning:" + gridView.getItemAnimator().isRunning()
- + " scrollState:" + gridView.getScrollState();
- }
-
- /**
- * Change selected position.
- */
- private void setSelectedPosition(final int position, final int scrollExtra) throws Throwable {
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPosition(position, scrollExtra);
- }
- });
- waitForLayout(false);
- }
-
- private void setSelectedPosition(final int position) throws Throwable {
- setSelectedPosition(position, 0);
- }
-
- private void setSelectedPositionSmooth(final int position) throws Throwable {
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(position);
- }
- });
- }
- /**
- * Scrolls using given key.
- */
- protected void scroll(int key, Runnable verify) throws Throwable {
- do {
- if (verify != null) {
- mActivityTestRule.runOnUiThread(verify);
- }
- sendRepeatedKeys(10, key);
- try {
- Thread.sleep(300);
- } catch (InterruptedException ex) {
- break;
- }
- } while (mGridView.getLayoutManager().isSmoothScrolling()
- || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE);
- }
-
- protected void scrollToBegin(Runnable verify) throws Throwable {
- int key;
- // first move to first column/row
- if (mOrientation == BaseGridView.HORIZONTAL) {
- key = KeyEvent.KEYCODE_DPAD_UP;
- } else {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- key = KeyEvent.KEYCODE_DPAD_RIGHT;
- } else {
- key = KeyEvent.KEYCODE_DPAD_LEFT;
- }
- }
- scroll(key, null);
- if (mOrientation == BaseGridView.HORIZONTAL) {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- key = KeyEvent.KEYCODE_DPAD_RIGHT;
- } else {
- key = KeyEvent.KEYCODE_DPAD_LEFT;
- }
- } else {
- key = KeyEvent.KEYCODE_DPAD_UP;
- }
- scroll(key, verify);
- }
-
- protected void scrollToEnd(Runnable verify) throws Throwable {
- int key;
- // first move to first column/row
- if (mOrientation == BaseGridView.HORIZONTAL) {
- key = KeyEvent.KEYCODE_DPAD_UP;
- } else {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- key = KeyEvent.KEYCODE_DPAD_RIGHT;
- } else {
- key = KeyEvent.KEYCODE_DPAD_LEFT;
- }
- }
- scroll(key, null);
- if (mOrientation == BaseGridView.HORIZONTAL) {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- key = KeyEvent.KEYCODE_DPAD_LEFT;
- } else {
- key = KeyEvent.KEYCODE_DPAD_RIGHT;
- }
- } else {
- key = KeyEvent.KEYCODE_DPAD_DOWN;
- }
- scroll(key, verify);
- }
-
- /**
- * Group and sort children by their position on each row (HORIZONTAL) or column(VERTICAL).
- */
- protected View[][] sortByRows() {
- final HashMap<Integer, ArrayList<View>> rows = new HashMap<Integer, ArrayList<View>>();
- ArrayList<Integer> rowLocations = new ArrayList<>();
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- View v = mGridView.getChildAt(i);
- int rowLocation;
- if (mOrientation == BaseGridView.HORIZONTAL) {
- rowLocation = v.getTop();
- } else {
- rowLocation = mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL
- ? v.getRight() : v.getLeft();
- }
- ArrayList<View> views = rows.get(rowLocation);
- if (views == null) {
- views = new ArrayList<View>();
- rows.put(rowLocation, views);
- rowLocations.add(rowLocation);
- }
- views.add(v);
- }
- Object[] sortedLocations = rowLocations.toArray();
- Arrays.sort(sortedLocations);
- if (mNumRows != rows.size()) {
- assertEquals("Dump Views by rows "+rows, mNumRows, rows.size());
- }
- View[][] sorted = new View[rows.size()][];
- for (int i = 0; i < rowLocations.size(); i++) {
- Integer rowLocation = rowLocations.get(i);
- ArrayList<View> arr = rows.get(rowLocation);
- View[] views = arr.toArray(new View[arr.size()]);
- Arrays.sort(views, mRowSortComparator);
- sorted[i] = views;
- }
- return sorted;
- }
-
- protected void verifyMargin() {
- View[][] sorted = sortByRows();
- for (int row = 0; row < sorted.length; row++) {
- View[] views = sorted[row];
- int margin = -1;
- for (int i = 1; i < views.length; i++) {
- if (mOrientation == BaseGridView.HORIZONTAL) {
- assertEquals(mGridView.getHorizontalMargin(),
- views[i].getLeft() - views[i - 1].getRight());
- } else {
- assertEquals(mGridView.getVerticalMargin(),
- views[i].getTop() - views[i - 1].getBottom());
- }
- }
- }
- }
-
- protected void verifyBeginAligned() {
- View[][] sorted = sortByRows();
- int alignedLocation = 0;
- if (mOrientation == BaseGridView.HORIZONTAL) {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- for (int i = 0; i < sorted.length; i++) {
- if (i == 0) {
- alignedLocation = sorted[i][sorted[i].length - 1].getRight();
- } else {
- assertEquals(alignedLocation, sorted[i][sorted[i].length - 1].getRight());
- }
- }
- } else {
- for (int i = 0; i < sorted.length; i++) {
- if (i == 0) {
- alignedLocation = sorted[i][0].getLeft();
- } else {
- assertEquals(alignedLocation, sorted[i][0].getLeft());
- }
- }
- }
- } else {
- for (int i = 0; i < sorted.length; i++) {
- if (i == 0) {
- alignedLocation = sorted[i][0].getTop();
- } else {
- assertEquals(alignedLocation, sorted[i][0].getTop());
- }
- }
- }
- }
-
- protected int[] getEndEdges() {
- View[][] sorted = sortByRows();
- int[] edges = new int[sorted.length];
- if (mOrientation == BaseGridView.HORIZONTAL) {
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- for (int i = 0; i < sorted.length; i++) {
- edges[i] = sorted[i][0].getLeft();
- }
- } else {
- for (int i = 0; i < sorted.length; i++) {
- edges[i] = sorted[i][sorted[i].length - 1].getRight();
- }
- }
- } else {
- for (int i = 0; i < sorted.length; i++) {
- edges[i] = sorted[i][sorted[i].length - 1].getBottom();
- }
- }
- return edges;
- }
-
- protected void verifyEdgesSame(int[] edges, int[] edges2) {
- assertEquals(edges.length, edges2.length);
- for (int i = 0; i < edges.length; i++) {
- assertEquals(edges[i], edges2[i]);
- }
- }
-
- protected void verifyBoundCount(int count) {
- if (mActivity.getBoundCount() != count) {
- StringBuffer b = new StringBuffer();
- b.append("ItemsLength: ");
- for (int i = 0; i < mActivity.mItemLengths.length; i++) {
- b.append(mActivity.mItemLengths[i]).append(",");
- }
- assertEquals("Bound count does not match, ItemsLengths: "+ b,
- count, mActivity.getBoundCount());
- }
- }
-
- private static int getCenterY(View v) {
- return (v.getTop() + v.getBottom())/2;
- }
-
- private static int getCenterX(View v) {
- return (v.getLeft() + v.getRight())/2;
- }
-
- private void initActivity(Intent intent) throws Throwable {
- mActivityTestRule = new ActivityTestRule<GridActivity>(GridActivity.class, false, false);
- mActivity = mActivityTestRule.launchActivity(intent);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.setTitle(testName.getMethodName());
- }
- });
- Thread.sleep(1000);
- mGridView = mActivity.mGridView;
- mLayoutManager = (GridLayoutManager) mGridView.getLayoutManager();
- }
-
- @After
- public void clearTest() {
- mWaitLayoutListener = null;
- mLayoutManager = null;
- mGridView = null;
- mActivity = null;
- mActivityTestRule = null;
- }
-
- /**
- * Must be called before waitForLayout() to prepare layout listener.
- */
- protected void startWaitLayout() {
- if (mWaitLayoutListener != null) {
- throw new IllegalStateException("startWaitLayout() already called");
- }
- if (mLayoutManager.mLayoutCompleteListener != null) {
- throw new IllegalStateException("Cannot startWaitLayout()");
- }
- mWaitLayoutListener = mLayoutManager.mLayoutCompleteListener =
- mock(GridLayoutManager.OnLayoutCompleteListener.class);
- }
-
- /**
- * wait layout to be called and remove the listener.
- */
- protected void waitForLayout() {
- waitForLayout(true);
- }
-
- /**
- * wait layout to be called and remove the listener.
- * @param force True if always wait regardless if layout requested
- */
- protected void waitForLayout(boolean force) {
- if (mWaitLayoutListener == null) {
- throw new IllegalStateException("startWaitLayout() not called");
- }
- if (mWaitLayoutListener != mLayoutManager.mLayoutCompleteListener) {
- throw new IllegalStateException("layout listener inconistent");
- }
- try {
- if (force || mGridView.isLayoutRequested()) {
- verify(mWaitLayoutListener, timeout(WAIT_FOR_LAYOUT_PASS_TIMEOUT_MS).atLeastOnce())
- .onLayoutCompleted(any(RecyclerView.State.class));
- }
- } finally {
- mWaitLayoutListener = null;
- mLayoutManager.mLayoutCompleteListener = null;
- }
- }
-
- /**
- * If currently running animator, wait for it to finish, otherwise return immediately.
- * To wait the ItemAnimator start, you can use waitForLayout() to make sure layout pass has
- * processed adapter change.
- */
- protected void waitForItemAnimation(int timeoutMs) throws Throwable {
- final RecyclerView.ItemAnimator.ItemAnimatorFinishedListener listener = mock(
- RecyclerView.ItemAnimator.ItemAnimatorFinishedListener.class);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().isRunning(listener);
- }
- });
- verify(listener, timeout(timeoutMs).atLeastOnce()).onAnimationsFinished();
- }
-
- protected void waitForItemAnimation() throws Throwable {
- waitForItemAnimation(WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS);
- }
-
- /**
- * wait animation start
- */
- protected void waitForItemAnimationStart() throws Throwable {
- long totalWait = 0;
- while (!mGridView.getItemAnimator().isRunning()) {
- Thread.sleep(10);
- if ((totalWait += 10) > WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS) {
- throw new RuntimeException("waitForItemAnimationStart Timeout");
- }
- }
- }
-
- /**
- * Run task in UI thread and wait for layout and ItemAnimator finishes.
- */
- protected void performAndWaitForAnimation(Runnable task) throws Throwable {
- startWaitLayout();
- mActivityTestRule.runOnUiThread(task);
- waitForLayout();
- waitForItemAnimation();
- }
-
- protected void waitForScrollIdle() throws Throwable {
- waitForScrollIdle(null);
- }
-
- /**
- * Wait for grid view stop scroll and optionally verify state of grid view.
- */
- protected void waitForScrollIdle(Runnable verify) throws Throwable {
- Thread.sleep(100);
- int total = 0;
- while (mGridView.getLayoutManager().isSmoothScrolling()
- || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
- if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
- throw new RuntimeException("waitForScrollIdle Timeout");
- }
- try {
- Thread.sleep(100);
- } catch (InterruptedException ex) {
- break;
- }
- if (verify != null) {
- mActivityTestRule.runOnUiThread(verify);
- }
- }
- }
-
- @Test
- public void testThreeRowHorizontalBasic() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- scrollToEnd(mVerifyLayout);
-
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
- }
-
- static class DividerDecoration extends RecyclerView.ItemDecoration {
-
- private ColorDrawable mTopDivider;
- private ColorDrawable mBottomDivider;
- private int mLeftOffset;
- private int mRightOffset;
- private int mTopOffset;
- private int mBottomOffset;
-
- DividerDecoration(int leftOffset, int topOffset, int rightOffset, int bottomOffset) {
- mLeftOffset = leftOffset;
- mTopOffset = topOffset;
- mRightOffset = rightOffset;
- mBottomOffset = bottomOffset;
- }
-
- @Override
- public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
- if (mTopDivider == null) {
- mTopDivider = new ColorDrawable(Color.RED);
- }
- if (mBottomDivider == null) {
- mBottomDivider = new ColorDrawable(Color.BLUE);
- }
- final int childCount = parent.getChildCount();
- final int width = parent.getWidth();
- for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) {
- final View view = parent.getChildAt(childViewIndex);
- mTopDivider.setBounds(0, (int) view.getY() - mTopOffset, width, (int) view.getY());
- mTopDivider.draw(c);
- mBottomDivider.setBounds(0, (int) view.getY() + view.getHeight(), width,
- (int) view.getY() + view.getHeight() + mBottomOffset);
- mBottomDivider.draw(c);
- }
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state) {
- outRect.left = mLeftOffset;
- outRect.top = mTopOffset;
- outRect.right = mRightOffset;
- outRect.bottom = mBottomOffset;
- }
- }
-
- @Test
- public void testItemDecorationAndMargins() throws Throwable {
-
- final int leftMargin = 3;
- final int topMargin = 4;
- final int rightMargin = 7;
- final int bottomMargin = 8;
- final int itemHeight = 100;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{itemHeight, itemHeight, itemHeight});
- intent.putExtra(GridActivity.EXTRA_LAYOUT_MARGINS,
- new int[]{leftMargin, topMargin, rightMargin, bottomMargin});
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- final int paddingLeft = mGridView.getPaddingLeft();
- final int paddingTop = mGridView.getPaddingTop();
- final int verticalSpace = mGridView.getVerticalMargin();
- final int decorationLeft = 17;
- final int decorationTop = 1;
- final int decorationRight = 19;
- final int decorationBottom = 2;
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mGridView.addItemDecoration(new DividerDecoration(decorationLeft, decorationTop,
- decorationRight, decorationBottom));
- }
- });
-
- View child0 = mGridView.getChildAt(0);
- View child1 = mGridView.getChildAt(1);
- View child2 = mGridView.getChildAt(2);
-
- assertEquals(itemHeight, child0.getBottom() - child0.getTop());
-
- // verify left margins
- assertEquals(paddingLeft + leftMargin + decorationLeft, child0.getLeft());
- assertEquals(paddingLeft + leftMargin + decorationLeft, child1.getLeft());
- assertEquals(paddingLeft + leftMargin + decorationLeft, child2.getLeft());
- // verify top bottom margins and decoration offset
- assertEquals(paddingTop + topMargin + decorationTop, child0.getTop());
- assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
- child1.getTop() - child0.getBottom());
- assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
- child2.getTop() - child1.getBottom());
-
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- public void testItemDecorationAndMarginsAndOpticalBounds() throws Throwable {
- final int leftMargin = 3;
- final int topMargin = 4;
- final int rightMargin = 7;
- final int bottomMargin = 8;
- final int itemHeight = 100;
- final int ninePatchDrawableResourceId = R.drawable.lb_card_shadow_focused;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{itemHeight, itemHeight, itemHeight});
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
- intent.putExtra(GridActivity.EXTRA_LAYOUT_MARGINS,
- new int[]{leftMargin, topMargin, rightMargin, bottomMargin});
- intent.putExtra(GridActivity.EXTRA_NINEPATCH_SHADOW, ninePatchDrawableResourceId);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- final int paddingLeft = mGridView.getPaddingLeft();
- final int paddingTop = mGridView.getPaddingTop();
- final int verticalSpace = mGridView.getVerticalMargin();
- final int decorationLeft = 17;
- final int decorationTop = 1;
- final int decorationRight = 19;
- final int decorationBottom = 2;
-
- final Rect opticalPaddings = new Rect();
- mGridView.getResources().getDrawable(ninePatchDrawableResourceId)
- .getPadding(opticalPaddings);
- final int opticalInsetsLeft = opticalPaddings.left;
- final int opticalInsetsTop = opticalPaddings.top;
- final int opticalInsetsRight = opticalPaddings.right;
- final int opticalInsetsBottom = opticalPaddings.bottom;
- assertTrue(opticalInsetsLeft > 0);
- assertTrue(opticalInsetsTop > 0);
- assertTrue(opticalInsetsRight > 0);
- assertTrue(opticalInsetsBottom > 0);
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mGridView.addItemDecoration(new DividerDecoration(decorationLeft, decorationTop,
- decorationRight, decorationBottom));
- }
- });
-
- View child0 = mGridView.getChildAt(0);
- View child1 = mGridView.getChildAt(1);
- View child2 = mGridView.getChildAt(2);
-
- assertEquals(itemHeight + opticalInsetsTop + opticalInsetsBottom,
- child0.getBottom() - child0.getTop());
-
- // verify left margins decoration and optical insets
- assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
- child0.getLeft());
- assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
- child1.getLeft());
- assertEquals(paddingLeft + leftMargin + decorationLeft - opticalInsetsLeft,
- child2.getLeft());
- // verify top bottom margins decoration offset and optical insets
- assertEquals(paddingTop + topMargin + decorationTop, child0.getTop() + opticalInsetsTop);
- assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
- (child1.getTop() + opticalInsetsTop) - (child0.getBottom() - opticalInsetsBottom));
- assertEquals(bottomMargin + decorationBottom + verticalSpace + decorationTop + topMargin,
- (child2.getTop() + opticalInsetsTop) - (child1.getBottom() - opticalInsetsBottom));
-
- }
-
- @Test
- public void testThreeColumnVerticalBasic() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- scrollToEnd(mVerifyLayout);
-
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
- }
-
- @Test
- public void testRedundantAppendRemove() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid_testredundantappendremove);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
- 149,177,128,234,227,187,163,223,146,210,228,148,227,193,182,197,177,142,225,207,
- 157,171,209,204,187,184,123,221,197,153,202,179,193,214,226,173,225,143,188,159,
- 139,193,233,143,227,203,222,124,228,223,164,131,228,126,211,160,165,152,235,184,
- 155,224,149,181,171,229,200,234,177,130,164,172,188,139,132,203,179,220,147,131,
- 226,127,230,239,183,203,206,227,123,170,239,234,200,149,237,204,160,133,202,234,
- 173,122,139,149,151,153,216,231,121,145,227,153,186,174,223,180,123,215,206,216,
- 239,222,219,207,193,218,140,133,171,153,183,132,233,138,159,174,189,171,143,128,
- 152,222,141,202,224,190,134,120,181,231,230,136,132,224,136,210,207,150,128,183,
- 221,194,179,220,126,221,137,205,223,193,172,132,226,209,133,191,227,127,159,171,
- 180,149,237,177,194,207,170,202,161,144,147,199,205,186,164,140,193,203,224,129});
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- scrollToEnd(mVerifyLayout);
-
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
- }
-
- @Test
- public void testRedundantAppendRemove2() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid_testredundantappendremove2);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
- 318,333,199,224,246,273,269,289,340,313,265,306,349,269,185,282,257,354,316,252,
- 237,290,283,343,196,313,290,343,191,262,342,228,343,349,251,203,226,305,265,213,
- 216,333,295,188,187,281,288,311,244,232,224,332,290,181,267,276,226,261,335,355,
- 225,217,219,183,234,285,257,304,182,250,244,223,257,219,342,185,347,205,302,315,
- 299,309,292,237,192,309,228,250,347,227,337,298,299,185,185,331,223,284,265,351});
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
- mLayoutManager = (GridLayoutManager) mGridView.getLayoutManager();
-
- // test append without staggered result cache
- scrollToEnd(mVerifyLayout);
-
- int[] endEdges = getEndEdges();
-
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
-
- // now test append with staggered result cache
- changeArraySize(3);
- assertEquals("Staggerd cache should be kept as is when no item size change",
- 100, ((StaggeredGrid) mLayoutManager.mGrid).mLocations.size());
-
- changeArraySize(100);
-
- scrollToEnd(mVerifyLayout);
-
- // we should get same aligned end edges
- int[] endEdges2 = getEndEdges();
- verifyEdgesSame(endEdges, endEdges2);
- }
-
-
- @Test
- public void testLayoutWhenAViewIsInvalidated() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
- intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, true);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mNumRows = 1;
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- waitOneUiCycle();
-
- // push views to cache.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.mItemLengths[0] = mActivity.mItemLengths[0] * 3;
- mActivity.mGridView.getAdapter().notifyItemChanged(0);
- }
- });
- waitForItemAnimation();
-
- // notifyDataSetChange will mark the cached views FLAG_INVALID
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.mGridView.getAdapter().notifyDataSetChanged();
- }
- });
- waitForItemAnimation();
-
- // Cached views will be added in prelayout with FLAG_INVALID, in post layout we should
- // handle it properly
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.mItemLengths[0] = mActivity.mItemLengths[0] / 3;
- mActivity.mGridView.getAdapter().notifyItemChanged(0);
- }
- });
-
- waitForItemAnimation();
- }
-
- @Test
- public void testWrongInsertViewIndexInFastRelayout() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mNumRows = 1;
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
-
- // removing two children, they will be hidden views as first 2 children of RV.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setRemoveDuration(2000);
- mActivity.removeItems(0, 2);
- }
- });
- waitForItemAnimationStart();
-
- // add three views and notify change of the first item.
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(0, new int[]{161, 161, 161});
- }
- });
- waitForLayout();
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyItemChanged(0);
- }
- });
- waitForLayout();
- // after layout, the viewholder should still be the first child of LayoutManager.
- assertEquals(0, mGridView.getChildAdapterPosition(
- mGridView.getLayoutManager().getChildAt(0)));
- }
-
- @Test
- public void testMoveIntoPrelayoutItems() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mNumRows = 1;
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
-
- final int lastItemPos = mGridView.getChildCount() - 1;
- assertTrue(mGridView.getChildCount() >= 4);
- // notify change of 3 items, so prelayout will layout extra 3 items, then move an item
- // into the extra layout range. Post layout's fastRelayout() should handle this properly.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyItemChanged(lastItemPos - 3);
- mGridView.getAdapter().notifyItemChanged(lastItemPos - 2);
- mGridView.getAdapter().notifyItemChanged(lastItemPos - 1);
- mActivity.moveItem(900, lastItemPos + 2, true);
- }
- });
- waitForItemAnimation();
- }
-
- @Test
- public void testMoveIntoPrelayoutItems2() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mNumRows = 1;
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
-
- setSelectedPosition(999);
- final int firstItemPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(0));
- assertTrue(mGridView.getChildCount() >= 4);
- // notify change of 3 items, so prelayout will layout extra 3 items, then move an item
- // into the extra layout range. Post layout's fastRelayout() should handle this properly.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyItemChanged(firstItemPos + 1);
- mGridView.getAdapter().notifyItemChanged(firstItemPos + 2);
- mGridView.getAdapter().notifyItemChanged(firstItemPos + 3);
- mActivity.moveItem(0, firstItemPos - 2, true);
- }
- });
- waitForItemAnimation();
- }
-
- void preparePredictiveLayout() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setAddDuration(1000);
- mGridView.getItemAnimator().setRemoveDuration(1000);
- mGridView.getItemAnimator().setMoveDuration(1000);
- mGridView.getItemAnimator().setChangeDuration(1000);
- mGridView.setSelectedPositionSmooth(50);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- }
-
- @Test
- public void testPredictiveLayoutAdd1() throws Throwable {
- preparePredictiveLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(51, new int[]{300, 300, 300, 300});
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- assertEquals(50, mGridView.getSelectedPosition());
- assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
- }
-
- @Test
- public void testPredictiveLayoutAdd2() throws Throwable {
- preparePredictiveLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(50, new int[]{300, 300, 300, 300});
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- assertEquals(54, mGridView.getSelectedPosition());
- assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
- }
-
- @Test
- public void testPredictiveLayoutRemove1() throws Throwable {
- preparePredictiveLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(51, 3);
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- assertEquals(50, mGridView.getSelectedPosition());
- assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
- }
-
- @Test
- public void testPredictiveLayoutRemove2() throws Throwable {
- preparePredictiveLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(47, 3);
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- assertEquals(47, mGridView.getSelectedPosition());
- assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
- }
-
- @Test
- public void testPredictiveLayoutRemove3() throws Throwable {
- preparePredictiveLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(0, 51);
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- assertEquals(0, mGridView.getSelectedPosition());
- assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
- }
-
- @Test
- public void testPredictiveOnMeasureWrapContent() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear_wrap_content);
- int count = 50;
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, count);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- waitForScrollIdle(mVerifyLayout);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setHasFixedSize(false);
- }
- });
-
- for (int i = 0; i < 30; i++) {
- final int oldCount = count;
- final int newCount = i;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (oldCount > 0) {
- mActivity.removeItems(0, oldCount);
- }
- if (newCount > 0) {
- int[] newItems = new int[newCount];
- for (int i = 0; i < newCount; i++) {
- newItems[i] = 400;
- }
- mActivity.addItems(0, newItems);
- }
- }
- });
- waitForItemAnimationStart();
- waitForItemAnimation();
- count = newCount;
- }
-
- }
-
- @Test
- public void testPredictiveLayoutRemove4() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(50);
- }
- });
- waitForScrollIdle();
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(0, 49);
- }
- });
- assertEquals(1, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testPredictiveLayoutRemove5() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(50);
- }
- });
- waitForScrollIdle();
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(50, 40);
- }
- });
- assertEquals(50, mGridView.getSelectedPosition());
- scrollToBegin(mVerifyLayout);
- verifyBeginAligned();
- }
-
- void waitOneUiCycle() throws Throwable {
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- }
- });
- }
-
- @Test
- public void testDontPruneMovingItem() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setMoveDuration(2000);
- mGridView.setSelectedPosition(50);
- }
- });
- waitForScrollIdle();
- final ArrayList<RecyclerView.ViewHolder> moveViewHolders = new ArrayList();
- for (int i = 51;; i++) {
- RecyclerView.ViewHolder vh = mGridView.findViewHolderForAdapterPosition(i);
- if (vh == null) {
- break;
- }
- moveViewHolders.add(vh);
- }
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // add a lot of items, so we will push everything to right of 51 out side window
- int[] lots_items = new int[1000];
- for (int i = 0; i < lots_items.length; i++) {
- lots_items[i] = 300;
- }
- mActivity.addItems(51, lots_items);
- }
- });
- waitOneUiCycle();
- // run a scroll pass, the scroll pass should not remove the animating views even they are
- // outside visible areas.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollBy(-3, 0);
- }
- });
- waitOneUiCycle();
- for (int i = 0; i < moveViewHolders.size(); i++) {
- assertSame(mGridView, moveViewHolders.get(i).itemView.getParent());
- }
- }
-
- @Test
- public void testMoveItemToTheRight() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setAddDuration(2000);
- mGridView.getItemAnimator().setMoveDuration(2000);
- mGridView.setSelectedPosition(50);
- }
- });
- waitForScrollIdle();
- RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(51);
-
- int lastPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(
- mGridView.getChildCount() - 1));
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.moveItem(51, 1000, true);
- }
- });
- final ArrayList<View> moveInViewHolders = new ArrayList();
- waitForItemAnimationStart();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
- View v = mGridView.getLayoutManager().getChildAt(i);
- if (mGridView.getChildAdapterPosition(v) >= 51) {
- moveInViewHolders.add(v);
- }
- }
- }
- });
- waitOneUiCycle();
- assertTrue("prelayout should layout extra items to slide in",
- moveInViewHolders.size() > lastPos - 51);
- // run a scroll pass, the scroll pass should not remove the animating views even they are
- // outside visible areas.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollBy(-3, 0);
- }
- });
- waitOneUiCycle();
- for (int i = 0; i < moveInViewHolders.size(); i++) {
- assertSame(mGridView, moveInViewHolders.get(i).getParent());
- }
- assertSame(mGridView, moveViewHolder.itemView.getParent());
- assertFalse(moveViewHolder.isRecyclable());
- waitForItemAnimation();
- assertNull(moveViewHolder.itemView.getParent());
- assertTrue(moveViewHolder.isRecyclable());
- }
-
- @Test
- public void testMoveItemToTheLeft() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setAddDuration(2000);
- mGridView.getItemAnimator().setMoveDuration(2000);
- mGridView.setSelectedPosition(1500);
- }
- });
- waitForScrollIdle();
- RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(1499);
-
- int firstPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(0));
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.moveItem(1499, 1, true);
- }
- });
- final ArrayList<View> moveInViewHolders = new ArrayList();
- waitForItemAnimationStart();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
- View v = mGridView.getLayoutManager().getChildAt(i);
- if (mGridView.getChildAdapterPosition(v) <= 1499) {
- moveInViewHolders.add(v);
- }
- }
- }
- });
- waitOneUiCycle();
- assertTrue("prelayout should layout extra items to slide in ",
- moveInViewHolders.size() > 1499 - firstPos);
- // run a scroll pass, the scroll pass should not remove the animating views even they are
- // outside visible areas.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollBy(3, 0);
- }
- });
- waitOneUiCycle();
- for (int i = 0; i < moveInViewHolders.size(); i++) {
- assertSame(mGridView, moveInViewHolders.get(i).getParent());
- }
- assertSame(mGridView, moveViewHolder.itemView.getParent());
- assertFalse(moveViewHolder.isRecyclable());
- waitForItemAnimation();
- assertNull(moveViewHolder.itemView.getParent());
- assertTrue(moveViewHolder.isRecyclable());
- }
-
- @Test
- public void testContinuousSwapForward() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(150);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- for (int i = 150; i < 199; i++) {
- final int swapIndex = i;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.swap(swapIndex, swapIndex + 1);
- }
- });
- Thread.sleep(10);
- }
- waitForItemAnimation();
- assertEquals(199, mGridView.getSelectedPosition());
- // check if ItemAnimation finishes at aligned positions:
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(199).getLeft();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(199).getLeft());
- }
-
- @Test
- public void testContinuousSwapBackward() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(50);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- for (int i = 50; i > 0; i--) {
- final int swapIndex = i;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.swap(swapIndex, swapIndex - 1);
- }
- });
- Thread.sleep(10);
- }
- waitForItemAnimation();
- assertEquals(0, mGridView.getSelectedPosition());
- // check if ItemAnimation finishes at aligned positions:
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(0).getLeft();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(0).getLeft());
- }
-
- @Test
- public void testScrollAndStuck() throws Throwable {
- // see b/67370222 fastRelayout() may be stuck.
- final int numItems = 19;
- final int[] itemsLength = new int[numItems];
- for (int i = 0; i < numItems; i++) {
- itemsLength[i] = 288;
- }
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_ITEMS, itemsLength);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- // set left right padding to 112, space between items to be 16.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ViewGroup.LayoutParams lp = mGridView.getLayoutParams();
- lp.width = 1920;
- mGridView.setLayoutParams(lp);
- mGridView.setPadding(112, mGridView.getPaddingTop(), 112,
- mGridView.getPaddingBottom());
- mGridView.setItemSpacing(16);
- }
- });
- waitOneUiCycle();
-
- int scrollPos = 0;
- while (true) {
- final View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
- final int pos = mGridView.getChildViewHolder(view).getAdapterPosition();
- if (scrollPos != pos) {
- scrollPos = pos;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.smoothScrollToPosition(pos);
- }
- });
- }
- // wait until we see 2nd from last:
- if (pos >= 17) {
- if (pos == 17) {
- // great we can test fastRelayout() bug.
- Thread.sleep(50);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- view.requestLayout();
- }
- });
- }
- break;
- }
- Thread.sleep(16);
- }
- waitForScrollIdle();
- }
-
- @Test
- public void testSwapAfterScroll() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setMoveDuration(1000);
- mGridView.setSelectedPositionSmooth(150);
- }
- });
- waitForScrollIdle();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(151);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // we want to swap and select new target which is at 150 before swap
- mGridView.setSelectedPositionSmooth(150);
- mActivity.swap(150, 151);
- }
- });
- waitForItemAnimation();
- waitForScrollIdle();
- assertEquals(151, mGridView.getSelectedPosition());
- // check if ItemAnimation finishes at aligned positions:
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(151).getLeft();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(151).getLeft());
- }
-
- @Test
- public void testItemMovedHorizontal() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(150);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.swap(150, 152);
- }
- });
- mActivityTestRule.runOnUiThread(mVerifyLayout);
-
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
- }
-
- @Test
- public void testItemMovedHorizontalRtl() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear_rtl);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[] {40, 40, 40});
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.moveItem(0, 1, true);
- }
- });
- assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
- mGridView.findViewHolderForAdapterPosition(0).itemView.getRight());
- }
-
- @Test
- public void testScrollSecondaryCannotScroll() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
- final int topPadding = 2;
- final int bottomPadding = 2;
- final int height = mGridView.getHeight();
- final int spacing = 2;
- final int rowHeight = (height - topPadding - bottomPadding) / 4 - spacing;
- final HorizontalGridView horizontalGridView = (HorizontalGridView) mGridView;
-
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- horizontalGridView.setPadding(0, topPadding, 0, bottomPadding);
- horizontalGridView.setItemSpacing(spacing);
- horizontalGridView.setNumRows(mNumRows);
- horizontalGridView.setRowHeight(rowHeight);
- }
- });
- waitForLayout();
- // navigate vertically in first column, first row should always be aligned to top padding
- for (int i = 0; i < 3; i++) {
- setSelectedPosition(i);
- assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView
- .getTop());
- }
- // navigate vertically in 100th column, first row should always be aligned to top padding
- for (int i = 300; i < 301; i++) {
- setSelectedPosition(i);
- assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(300).itemView
- .getTop());
- }
- }
-
- @Test
- public void testScrollSecondaryNeedScroll() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- // test a lot of rows so we have to scroll vertically to reach
- mNumRows = 9;
- final int topPadding = 2;
- final int bottomPadding = 2;
- final int height = mGridView.getHeight();
- final int spacing = 2;
- final int rowHeight = (height - topPadding - bottomPadding) / 4 - spacing;
- final HorizontalGridView horizontalGridView = (HorizontalGridView) mGridView;
-
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- horizontalGridView.setPadding(0, topPadding, 0, bottomPadding);
- horizontalGridView.setItemSpacing(spacing);
- horizontalGridView.setNumRows(mNumRows);
- horizontalGridView.setRowHeight(rowHeight);
- }
- });
- waitForLayout();
- View view;
- // first row should be aligned to top padding
- setSelectedPosition(0);
- assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView.getTop());
- // middle row should be aligned to keyline (1/2 of screen height)
- setSelectedPosition(4);
- view = mGridView.findViewHolderForAdapterPosition(4).itemView;
- assertEquals(height / 2, (view.getTop() + view.getBottom()) / 2);
- // last row should be aligned to bottom padding.
- setSelectedPosition(8);
- view = mGridView.findViewHolderForAdapterPosition(8).itemView;
- assertEquals(height, view.getTop() + rowHeight + bottomPadding);
- setSelectedPositionSmooth(4);
- waitForScrollIdle();
- // middle row should be aligned to keyline (1/2 of screen height)
- setSelectedPosition(4);
- view = mGridView.findViewHolderForAdapterPosition(4).itemView;
- assertEquals(height / 2, (view.getTop() + view.getBottom()) / 2);
- // first row should be aligned to top padding
- setSelectedPositionSmooth(0);
- waitForScrollIdle();
- assertEquals(topPadding, mGridView.findViewHolderForAdapterPosition(0).itemView.getTop());
- }
-
- @Test
- public void testItemMovedVertical() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- mGridView.setSelectedPositionSmooth(150);
- waitForScrollIdle(mVerifyLayout);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.swap(150, 152);
- }
- });
- mActivityTestRule.runOnUiThread(mVerifyLayout);
-
- scrollToEnd(mVerifyLayout);
- scrollToBegin(mVerifyLayout);
-
- verifyBeginAligned();
- }
-
- @Test
- public void testAddLastItemHorizontal() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(49);
- }
- }
- );
- waitForScrollIdle(mVerifyLayout);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(50, new int[]{150});
- }
- });
-
- // assert new added item aligned to right edge
- assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
- mGridView.getLayoutManager().findViewByPosition(50).getRight());
- }
-
- @Test
- public void testAddMultipleLastItemsHorizontal() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- mActivityTestRule.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_BOTH_EDGE);
- mGridView.setWindowAlignmentOffsetPercent(50);
- mGridView.setSelectedPositionSmooth(49);
- }
- }
- );
- waitForScrollIdle(mVerifyLayout);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(50, new int[]{150, 150, 150, 150, 150, 150, 150, 150, 150,
- 150, 150, 150, 150, 150});
- }
- });
-
- // The focused item will be at center of window
- View view = mGridView.getLayoutManager().findViewByPosition(49);
- assertEquals(mGridView.getWidth() / 2, (view.getLeft() + view.getRight()) / 2);
- }
-
- @Test
- public void testItemAddRemoveHorizontal() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- scrollToEnd(mVerifyLayout);
- int[] endEdges = getEndEdges();
-
- mGridView.setSelectedPositionSmooth(150);
- waitForScrollIdle(mVerifyLayout);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mRemovedItems = mActivity.removeItems(151, 4);
- }
- });
-
- scrollToEnd(mVerifyLayout);
- mGridView.setSelectedPositionSmooth(150);
- waitForScrollIdle(mVerifyLayout);
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(151, mRemovedItems);
- }
- });
- scrollToEnd(mVerifyLayout);
-
- // we should get same aligned end edges
- int[] endEdges2 = getEndEdges();
- verifyEdgesSame(endEdges, endEdges2);
-
- scrollToBegin(mVerifyLayout);
- verifyBeginAligned();
- }
-
- @Test
- public void testSetSelectedPositionDetached() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int focusToIndex = 49;
- final ViewGroup parent = (ViewGroup) mGridView.getParent();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- parent.removeView(mGridView);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- parent.addView(mGridView);
- mGridView.requestFocus();
- }
- });
- waitForScrollIdle();
- assertEquals(mGridView.getSelectedPosition(), focusToIndex);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(focusToIndex).hasFocus());
-
- final int focusToIndex2 = 0;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- parent.removeView(mGridView);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPosition(focusToIndex2);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- parent.addView(mGridView);
- mGridView.requestFocus();
- }
- });
- assertEquals(mGridView.getSelectedPosition(), focusToIndex2);
- waitForScrollIdle();
- assertTrue(mGridView.getLayoutManager().findViewByPosition(focusToIndex2).hasFocus());
- }
-
- @Test
- public void testBug22209986() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int focusToIndex = mGridView.getChildCount() - 1;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- waitForScrollIdle();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex + 1);
- }
- });
- // let the scroll running for a while and requestLayout during scroll
- Thread.sleep(80);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- assertEquals(mGridView.getScrollState(), BaseGridView.SCROLL_STATE_SETTLING);
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
-
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge,
- mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
- }
-
- void testScrollAndRemove(int[] itemsLength, int numItems) throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- if (itemsLength != null) {
- intent.putExtra(GridActivity.EXTRA_ITEMS, itemsLength);
- } else {
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- }
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int focusToIndex = mGridView.getChildCount() - 1;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(focusToIndex, 1);
- }
- });
-
- waitOneUiCycle();
- waitForScrollIdle();
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge,
- mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft(), DELTA);
- }
-
- @Test
- public void testScrollAndRemove() throws Throwable {
- // test random lengths for 50 items
- testScrollAndRemove(null, 50);
- }
-
- /**
- * This test verifies if scroll limits are ignored when onLayoutChildren compensate remaining
- * scroll distance. b/64931938
- * In the test, second child is long, other children are short.
- * Test scrolls to the long child, and when scrolling, remove the long child. We made it long
- * to have enough remaining scroll distance when the layout pass kicks in.
- * The onLayoutChildren() would compensate the remaining scroll distance, moving all items
- * toward right, which will make the first item's left edge bigger than left padding,
- * which would violate the "scroll limit of left" in a regular scroll case, but
- * in layout pass, we still honor that scroll request, ignoring the scroll limit.
- */
- @Test
- public void testScrollAndRemoveSample1() throws Throwable {
- DisplayMetrics dm = InstrumentationRegistry.getInstrumentation().getTargetContext()
- .getResources().getDisplayMetrics();
- // screen width for long item and 4DP for other items
- int longItemLength = dm.widthPixels;
- int shortItemLength = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, dm);
- int[] items = new int[1000];
- for (int i = 0; i < items.length; i++) {
- items[i] = shortItemLength;
- }
- items[1] = longItemLength;
- testScrollAndRemove(items, 0);
- }
-
- @Test
- public void testScrollAndInsert() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- int[] items = new int[1000];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300 + (int)(Math.random() * 100);
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(150);
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
- final int focusToIndex = mGridView.getChildAdapterPosition(view);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- int[] newItems = new int[]{300, 300, 300};
- mActivity.addItems(0, newItems);
- }
- });
- waitForScrollIdle();
- int topEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(topEdge,
- mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop());
- }
-
- @Test
- public void testScrollAndInsertBeforeVisibleItem() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- int[] items = new int[1000];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300 + (int)(Math.random() * 100);
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(150);
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- View view = mGridView.getChildAt(mGridView.getChildCount() - 1);
- final int focusToIndex = mGridView.getChildAdapterPosition(view);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- int[] newItems = new int[]{300, 300, 300};
- mActivity.addItems(focusToIndex, newItems);
- }
- });
- }
-
- @Test
- public void testSmoothScrollAndRemove() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int focusToIndex = 200;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(focusToIndex, 1);
- }
- });
-
- assertTrue("removing the index of not attached child should not affect smooth scroller",
- mGridView.getLayoutManager().isSmoothScrolling());
- waitForScrollIdle();
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge,
- mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
- }
-
- @Test
- public void testSmoothScrollAndRemove2() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int focusToIndex = 200;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusToIndex);
- }
- });
-
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final int removeIndex = mGridView.getChildViewHolder(
- mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
- mActivity.removeItems(removeIndex, 1);
- }
- });
- waitForLayout();
-
- assertTrue("removing the index of attached child should not kill smooth scroller",
- mGridView.getLayoutManager().isSmoothScrolling());
- waitForItemAnimation();
- waitForScrollIdle();
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(leftEdge,
- mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft());
- }
-
- @Test
- public void testPendingSmoothScrollAndRemove() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 630 + (int)(Math.random() * 100);
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
- assertTrue(mGridView.getChildAt(0).hasFocus());
-
- // Pressing lots of key to make sure smooth scroller is running
- mGridView.mLayoutManager.mMaxPendingMoves = 100;
- for (int i = 0; i < 100; i++) {
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- }
-
- assertTrue(mGridView.getLayoutManager().isSmoothScrolling());
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final int removeIndex = mGridView.getChildViewHolder(
- mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
- mActivity.removeItems(removeIndex, 1);
- }
- });
- waitForLayout();
-
- assertTrue("removing the index of attached child should not kill smooth scroller",
- mGridView.getLayoutManager().isSmoothScrolling());
-
- waitForItemAnimation();
- waitForScrollIdle();
- int focusIndex = mGridView.getSelectedPosition();
- int topEdge = mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- }
- });
- waitForScrollIdle();
- assertEquals(topEdge,
- mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop());
- }
-
- @Test
- public void testFocusToFirstItem() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mRemovedItems = mActivity.removeItems(0, 200);
- }
- });
-
- humanDelay(500);
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(0, mRemovedItems);
- }
- });
-
- humanDelay(500);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(0).hasFocus());
-
- changeArraySize(0);
-
- changeArraySize(200);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(0).hasFocus());
- }
-
- @Test
- public void testNonFocusableHorizontal() throws Throwable {
- final int numItems = 200;
- final int startPos = 45;
- final int skips = 20;
- final int numColumns = 3;
- final int endPos = startPos + numColumns * (skips + 1);
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = true;
- }
- for (int i = startPos + mNumRows, j = 0; j < skips; i += mNumRows, j++) {
- focusable[i] = false;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(startPos);
- waitForScrollIdle(mVerifyLayout);
-
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
- } else {
- sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- }
- waitForScrollIdle(mVerifyLayout);
- assertEquals(endPos, mGridView.getSelectedPosition());
-
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- } else {
- sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
- }
- waitForScrollIdle(mVerifyLayout);
- assertEquals(startPos, mGridView.getSelectedPosition());
-
- }
-
- @Test
- public void testNoInitialFocusable() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- final int numItems = 100;
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
- boolean[] focusable = new boolean[numItems];
- final int firstFocusableIndex = 10;
- for (int i = 0; i < firstFocusableIndex; i++) {
- focusable[i] = false;
- }
- for (int i = firstFocusableIndex; i < focusable.length; i++) {
- focusable[i] = true;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
- assertTrue(mGridView.isFocused());
-
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
- } else {
- sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- }
- waitForScrollIdle(mVerifyLayout);
- assertEquals(firstFocusableIndex, mGridView.getSelectedPosition());
- assertTrue(mGridView.getLayoutManager().findViewByPosition(firstFocusableIndex).hasFocus());
- }
-
- @Test
- public void testFocusOutOfEmptyListView() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- final int numItems = 100;
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
- initActivity(intent);
-
- final View horizontalGridView = new HorizontalGridViewEx(mGridView.getContext());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- horizontalGridView.setFocusable(true);
- horizontalGridView.setFocusableInTouchMode(true);
- horizontalGridView.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
- ((ViewGroup) mGridView.getParent()).addView(horizontalGridView, 0);
- horizontalGridView.requestFocus();
- }
- });
-
- assertTrue(horizontalGridView.isFocused());
-
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
-
- assertTrue(mGridView.hasFocus());
- }
-
- @Test
- public void testTransferFocusToChildWhenGainFocus() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- final int numItems = 100;
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
- boolean[] focusable = new boolean[numItems];
- final int firstFocusableIndex = 1;
- for (int i = 0; i < firstFocusableIndex; i++) {
- focusable[i] = false;
- }
- for (int i = firstFocusableIndex; i < focusable.length; i++) {
- focusable[i] = true;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- assertEquals(firstFocusableIndex, mGridView.getSelectedPosition());
- assertTrue(mGridView.getLayoutManager().findViewByPosition(firstFocusableIndex).hasFocus());
- }
-
- @Test
- public void testFocusFromSecondChild() throws Throwable {
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- final int numItems = 100;
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = false;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- // switching Adapter to cause a full rebind, test if it will focus to second item.
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.mNumItems = numItems;
- mActivity.mItemFocusables[1] = true;
- mActivity.rebindToNewAdapter();
- }
- });
- assertTrue(mGridView.findViewHolderForAdapterPosition(1).itemView.hasFocus());
- }
-
- @Test
- public void removeFocusableItemAndFocusableRecyclerViewGetsFocus() throws Throwable {
- final int numItems = 100;
- final int numColumns = 3;
- final int focusableIndex = 2;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = false;
- }
- focusable[focusableIndex] = true;
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(focusableIndex);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(focusableIndex, mGridView.getSelectedPosition());
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(focusableIndex, 1);
- }
- });
- assertTrue(dumpGridView(mGridView), mGridView.isFocused());
- }
-
- @Test
- public void removeFocusableItemAndUnFocusableRecyclerViewLosesFocus() throws Throwable {
- final int numItems = 100;
- final int numColumns = 3;
- final int focusableIndex = 2;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = false;
- }
- focusable[focusableIndex] = true;
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setFocusableInTouchMode(false);
- mGridView.setFocusable(false);
- mGridView.setSelectedPositionSmooth(focusableIndex);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(focusableIndex, mGridView.getSelectedPosition());
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(focusableIndex, 1);
- }
- });
- assertFalse(dumpGridView(mGridView), mGridView.hasFocus());
- }
-
- @Test
- public void testNonFocusableVertical() throws Throwable {
- final int numItems = 200;
- final int startPos = 44;
- final int skips = 20;
- final int numColumns = 3;
- final int endPos = startPos + numColumns * (skips + 1);
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = true;
- }
- for (int i = startPos + mNumRows, j = 0; j < skips; i += mNumRows, j++) {
- focusable[i] = false;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(startPos);
- waitForScrollIdle(mVerifyLayout);
-
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(mVerifyLayout);
- assertEquals(endPos, mGridView.getSelectedPosition());
-
- sendKey(KeyEvent.KEYCODE_DPAD_UP);
- waitForScrollIdle(mVerifyLayout);
- assertEquals(startPos, mGridView.getSelectedPosition());
-
- }
-
- @Test
- public void testLtrFocusOutStartDisabled() throws Throwable {
- final int numItems = 200;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid_ltr);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestFocus();
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
- waitForScrollIdle(mVerifyLayout);
- assertTrue(mGridView.hasFocus());
- }
-
- @Test
- public void testRtlFocusOutStartDisabled() throws Throwable {
- final int numItems = 200;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid_rtl);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestFocus();
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- sendKey(KeyEvent.KEYCODE_DPAD_RIGHT);
- waitForScrollIdle(mVerifyLayout);
- assertTrue(mGridView.hasFocus());
- }
-
- @Test
- public void testTransferFocusable() throws Throwable {
- final int numItems = 200;
- final int numColumns = 3;
- final int startPos = 1;
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = true;
- }
- for (int i = 0; i < startPos; i++) {
- focusable[i] = false;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- changeArraySize(0);
- assertTrue(mGridView.isFocused());
-
- changeArraySize(numItems);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
- }
-
- @Test
- public void testTransferFocusable2() throws Throwable {
- final int numItems = 200;
- final int numColumns = 3;
- final int startPos = 3; // make sure view at startPos is in visible area.
-
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = numColumns;
- boolean[] focusable = new boolean[numItems];
- for (int i = 0; i < focusable.length; i++) {
- focusable[i] = true;
- }
- for (int i = 0; i < startPos; i++) {
- focusable[i] = false;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
- initActivity(intent);
-
- assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
-
- changeArraySize(0);
- assertTrue(mGridView.isFocused());
-
- changeArraySize(numItems);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
- }
-
- @Test
- public void testNonFocusableLoseInFastLayout() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- int[] items = new int[300];
- for (int i = 0; i < items.length; i++) {
- items[i] = 480;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
- int pressDown = 15;
-
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
-
- for (int i = 0; i < pressDown; i++) {
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- }
- waitForScrollIdle(mVerifyLayout);
- assertFalse(mGridView.isFocused());
-
- }
-
- @Test
- public void testFocusableViewAvailable() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE,
- new boolean[]{false, false, true, false, false});
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // RecyclerView does not respect focusable and focusableInTouchMode flag, so
- // set flags in code.
- mGridView.setFocusableInTouchMode(false);
- mGridView.setFocusable(false);
- }
- });
-
- assertFalse(mGridView.isFocused());
-
- final boolean[] scrolled = new boolean[]{false};
- mGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy){
- if (dy > 0) {
- scrolled[0] = true;
- }
- }
- });
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(0, new int[]{200, 300, 500, 500, 200});
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- assertFalse("GridView should not be scrolled", scrolled[0]);
- assertTrue(mGridView.getLayoutManager().findViewByPosition(2).hasFocus());
-
- }
-
- @Test
- public void testSetSelectionWithDelta() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(3);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int top1 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
-
- humanDelay(1000);
-
- // scroll to position with delta
- setSelectedPosition(3, 100);
- int top2 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
- assertEquals(top1 - 100, top2);
-
- // scroll to same position without delta, it will be reset
- setSelectedPosition(3, 0);
- int top3 = mGridView.getLayoutManager().findViewByPosition(3).getTop();
- assertEquals(top1, top3);
-
- // scroll invisible item after last visible item
- final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getLastVisibleIndex();
- setSelectedPosition(lastVisiblePos + 1, 100);
- int top4 = mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1).getTop();
- assertEquals(top1 - 100, top4);
-
- // scroll invisible item before first visible item
- final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getFirstVisibleIndex();
- setSelectedPosition(firstVisiblePos - 1, 100);
- int top5 = mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1).getTop();
- assertEquals(top1 - 100, top5);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(50, 100);
- int top6 = mGridView.getLayoutManager().findViewByPosition(50).getTop();
- assertEquals(top1 - 100, top6);
-
- // scroll to invisible item that is far away.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(100);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int top7 = mGridView.getLayoutManager().findViewByPosition(100).getTop();
- assertEquals(top1, top7);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(10, 50);
- int top8 = mGridView.getLayoutManager().findViewByPosition(10).getTop();
- assertEquals(top1 - 50, top8);
- }
-
- @Test
- public void testSetSelectionWithDeltaInGrid() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(10);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int top1 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
-
- humanDelay(500);
-
- // scroll to position with delta
- setSelectedPosition(20, 100);
- int top2 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
- assertEquals(top1 - 100, top2);
-
- // scroll to same position without delta, it will be reset
- setSelectedPosition(20, 0);
- int top3 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
- assertEquals(top1, top3);
-
- // scroll invisible item after last visible item
- final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getLastVisibleIndex();
- setSelectedPosition(lastVisiblePos + 1, 100);
- int top4 = getCenterY(mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1));
- verifyMargin();
- assertEquals(top1 - 100, top4);
-
- // scroll invisible item before first visible item
- final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getFirstVisibleIndex();
- setSelectedPosition(firstVisiblePos - 1, 100);
- int top5 = getCenterY(mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1));
- assertEquals(top1 - 100, top5);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(100, 100);
- int top6 = getCenterY(mGridView.getLayoutManager().findViewByPosition(100));
- assertEquals(top1 - 100, top6);
-
- // scroll to invisible item that is far away.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(200);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- Thread.sleep(500);
- int top7 = getCenterY(mGridView.getLayoutManager().findViewByPosition(200));
- assertEquals(top1, top7);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(10, 50);
- int top8 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
- assertEquals(top1 - 50, top8);
- }
-
-
- @Test
- public void testSetSelectionWithDeltaInGrid1() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{
- 193,176,153,141,203,184,232,139,177,206,222,136,132,237,172,137,
- 188,172,163,213,158,219,209,147,133,229,170,197,138,215,188,205,
- 223,192,225,170,195,127,229,229,210,195,134,142,160,139,130,222,
- 150,163,180,176,157,137,234,169,159,167,182,150,224,231,202,236,
- 123,140,181,223,120,185,183,221,123,210,134,158,166,208,149,128,
- 192,214,212,198,133,140,158,133,229,173,226,141,180,128,127,218,
- 192,235,183,213,216,150,143,193,125,141,219,210,195,195,192,191,
- 212,236,157,189,160,220,147,158,220,199,233,231,201,180,168,141,
- 156,204,191,183,190,153,123,210,238,151,139,221,223,200,175,191,
- 132,184,197,204,236,157,230,151,195,219,212,143,172,149,219,184,
- 164,211,132,187,172,142,174,146,127,147,206,238,188,129,199,226,
- 132,220,210,159,235,153,208,182,196,123,180,159,131,135,175,226,
- 127,134,237,211,133,225,132,124,160,226,224,200,173,137,217,169,
- 182,183,176,185,122,168,195,159,172,129,126,129,166,136,149,220,
- 178,191,192,238,180,208,234,154,222,206,239,228,129,140,203,125,
- 214,175,125,169,196,132,234,138,192,142,234,190,215,232,239,122,
- 188,158,128,221,159,237,207,157,232,138,132,214,122,199,121,191,
- 199,209,126,164,175,187,173,186,194,224,191,196,146,208,213,210,
- 164,176,202,213,123,157,179,138,217,129,186,166,237,211,157,130,
- 137,132,171,232,216,239,180,151,137,132,190,133,218,155,171,227,
- 193,147,197,164,120,218,193,154,170,196,138,222,161,235,143,154,
- 192,178,228,195,178,133,203,178,173,206,178,212,136,157,169,124,
- 172,121,128,223,238,125,217,187,184,156,169,215,231,124,210,174,
- 146,226,185,134,223,228,183,182,136,133,199,146,180,233,226,225,
- 174,233,145,235,216,170,192,171,132,132,134,223,233,148,154,162,
- 192,179,197,203,139,197,174,187,135,132,180,136,192,195,124,221,
- 120,189,233,233,146,225,234,163,215,143,132,198,156,205,151,190,
- 204,239,221,229,123,138,134,217,219,136,218,215,167,139,195,125,
- 202,225,178,226,145,208,130,194,228,197,157,215,124,147,174,123,
- 237,140,172,181,161,151,229,216,199,199,179,213,146,122,222,162,
- 139,173,165,150,160,217,207,137,165,175,129,158,134,133,178,199,
- 215,213,122,197
- });
- intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(10);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int top1 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
-
- humanDelay(500);
-
- // scroll to position with delta
- setSelectedPosition(20, 100);
- int top2 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
- assertEquals(top1 - 100, top2);
-
- // scroll to same position without delta, it will be reset
- setSelectedPosition(20, 0);
- int top3 = getCenterY(mGridView.getLayoutManager().findViewByPosition(20));
- assertEquals(top1, top3);
-
- // scroll invisible item after last visible item
- final int lastVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getLastVisibleIndex();
- setSelectedPosition(lastVisiblePos + 1, 100);
- int top4 = getCenterY(mGridView.getLayoutManager().findViewByPosition(lastVisiblePos + 1));
- verifyMargin();
- assertEquals(top1 - 100, top4);
-
- // scroll invisible item before first visible item
- final int firstVisiblePos = ((GridLayoutManager)mGridView.getLayoutManager())
- .mGrid.getFirstVisibleIndex();
- setSelectedPosition(firstVisiblePos - 1, 100);
- int top5 = getCenterY(mGridView.getLayoutManager().findViewByPosition(firstVisiblePos - 1));
- assertEquals(top1 - 100, top5);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(100, 100);
- int top6 = getCenterY(mGridView.getLayoutManager().findViewByPosition(100));
- assertEquals(top1 - 100, top6);
-
- // scroll to invisible item that is far away.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(200);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- Thread.sleep(500);
- int top7 = getCenterY(mGridView.getLayoutManager().findViewByPosition(200));
- assertEquals(top1, top7);
-
- // scroll to invisible item that is far away.
- setSelectedPosition(10, 50);
- int top8 = getCenterY(mGridView.getLayoutManager().findViewByPosition(10));
- assertEquals(top1 - 50, top8);
- }
-
- @Test
- public void testSmoothScrollSelectionEvents() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(30);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- humanDelay(500);
-
- final ArrayList<Integer> selectedPositions = new ArrayList<Integer>();
- mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
- @Override
- public void onChildSelected(ViewGroup parent, View view, int position, long id) {
- selectedPositions.add(position);
- }
- });
-
- sendRepeatedKeys(10, KeyEvent.KEYCODE_DPAD_UP);
- humanDelay(500);
- waitForScrollIdle(mVerifyLayout);
- // should only get childselected event for item 0 once
- assertTrue(selectedPositions.size() > 0);
- assertEquals(0, selectedPositions.get(selectedPositions.size() - 1).intValue());
- for (int i = selectedPositions.size() - 2; i >= 0; i--) {
- assertFalse(0 == selectedPositions.get(i).intValue());
- }
-
- }
-
- @Test
- public void testSmoothScrollSelectionEventsLinear() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 500);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(10);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- humanDelay(500);
-
- final ArrayList<Integer> selectedPositions = new ArrayList<Integer>();
- mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
- @Override
- public void onChildSelected(ViewGroup parent, View view, int position, long id) {
- selectedPositions.add(position);
- }
- });
-
- sendRepeatedKeys(10, KeyEvent.KEYCODE_DPAD_UP);
- humanDelay(500);
- waitForScrollIdle(mVerifyLayout);
- // should only get childselected event for item 0 once
- assertTrue(selectedPositions.size() > 0);
- assertEquals(0, selectedPositions.get(selectedPositions.size() - 1).intValue());
- for (int i = selectedPositions.size() - 2; i >= 0; i--) {
- assertFalse(0 == selectedPositions.get(i).intValue());
- }
-
- }
-
- @Test
- public void testScrollToNoneExisting() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 3;
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(99);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- humanDelay(500);
-
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(50);
- }
- });
- Thread.sleep(100);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestLayout();
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- humanDelay(500);
-
- }
-
- @Test
- public void testSmoothscrollerInterrupted() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 680;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
- assertTrue(mGridView.getChildAt(0).hasFocus());
-
- // Pressing lots of key to make sure smooth scroller is running
- for (int i = 0; i < 20; i++) {
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- }
- while (mGridView.getLayoutManager().isSmoothScrolling()
- || mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
- // Repeatedly pressing to make sure pending keys does not drop to zero.
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- }
- }
-
- @Test
- public void testSmoothscrollerCancelled() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 680;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
- assertTrue(mGridView.getChildAt(0).hasFocus());
-
- int targetPosition = items.length - 1;
- mGridView.setSelectedPositionSmooth(targetPosition);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.stopScroll();
- }
- });
- waitForScrollIdle();
- waitForItemAnimation();
- assertEquals(mGridView.getSelectedPosition(), targetPosition);
- assertSame(mGridView.getLayoutManager().findViewByPosition(targetPosition),
- mGridView.findFocus());
- }
-
- @Test
- public void testSetNumRowsAndAddItem() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[2];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
-
- mActivity.addItems(items.length, new int[]{300});
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ((VerticalGridView) mGridView).setNumColumns(2);
- }
- });
- Thread.sleep(1000);
- assertTrue(mGridView.getChildAt(2).getLeft() != mGridView.getChildAt(1).getLeft());
- }
-
-
- @Test
- public void testRequestLayoutBugInLayout() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(1);
- }
- });
- waitForScrollIdle(mVerifyLayout);
-
- sendKey(KeyEvent.KEYCODE_DPAD_UP);
- waitForScrollIdle(mVerifyLayout);
-
- assertEquals("Line 2", ((TextView) mGridView.findFocus()).getText().toString());
- }
-
-
- @Test
- public void testChangeLayoutInChild() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_wrap_content);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- int[] items = new int[2];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- verifyMargin();
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(1);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- verifyMargin();
- }
-
- @Test
- public void testWrapContent() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_grid_wrap);
- int[] items = new int[200];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.attachToNewAdapter(new int[0]);
- }
- });
-
- }
-
- @Test
- public void testZeroFixedSecondarySize() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_measured_with_zero);
- intent.putExtra(GridActivity.EXTRA_SECONDARY_SIZE_ZERO, true);
- int[] items = new int[2];
- for (int i = 0; i < items.length; i++) {
- items[i] = 0;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- }
-
- @Test
- public void testChildStates() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 200;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_ALL_CHILD);
-
- final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
-
- // 1 Save view states
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(0))
- .getText()), 0, 1);
- Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(1))
- .getText()), 0, 1);
- mGridView.saveHierarchyState(container);
- }
- });
-
- // 2 Change view states
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(0))
- .getText()), 1, 2);
- Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(1))
- .getText()), 1, 2);
- }
- });
-
- // 3 Detached and re-attached, should still maintain state of (2)
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(1);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionStart(), 1);
- assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionEnd(), 2);
- assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionStart(), 1);
- assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionEnd(), 2);
-
- // 4 Recycled and rebound, should load state from (2)
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(20);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionStart(), 1);
- assertEquals(((TextView) mGridView.getChildAt(0)).getSelectionEnd(), 2);
- assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionStart(), 1);
- assertEquals(((TextView) mGridView.getChildAt(1)).getSelectionEnd(), 2);
- }
-
-
- @Test
- public void testNoDispatchSaveChildState() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 200;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_NO_CHILD);
-
- final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
-
- // 1. Set text selection, save view states should do nothing on child
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- Selection.setSelection((Spannable)(((TextView) mGridView.getChildAt(i))
- .getText()), 0, 1);
- }
- mGridView.saveHierarchyState(container);
- }
- });
-
- // 2. clear the text selection
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- Selection.removeSelection((Spannable)(((TextView) mGridView.getChildAt(i))
- .getText()));
- }
- }
- });
-
- // 3. Restore view states should be a no-op for child
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.restoreHierarchyState(container);
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- assertEquals(-1, ((TextView) mGridView.getChildAt(i)).getSelectionStart());
- assertEquals(-1, ((TextView) mGridView.getChildAt(i)).getSelectionEnd());
- }
- }
- });
- }
-
-
- static interface ViewTypeProvider {
- public int getViewType(int position);
- }
-
- static interface ItemAlignmentFacetProvider {
- public ItemAlignmentFacet getItemAlignmentFacet(int viewType);
- }
-
- static class TwoViewTypesProvider implements ViewTypeProvider {
- static int VIEW_TYPE_FIRST = 1;
- static int VIEW_TYPE_DEFAULT = 0;
- @Override
- public int getViewType(int position) {
- if (position == 0) {
- return VIEW_TYPE_FIRST;
- } else {
- return VIEW_TYPE_DEFAULT;
- }
- }
- }
-
- static class ChangeableViewTypesProvider implements ViewTypeProvider {
- static SparseIntArray sViewTypes = new SparseIntArray();
- @Override
- public int getViewType(int position) {
- return sViewTypes.get(position);
- }
- public static void clear() {
- sViewTypes.clear();
- }
- public static void setViewType(int position, int type) {
- sViewTypes.put(position, type);
- }
- }
-
- static class PositionItemAlignmentFacetProviderForRelativeLayout1
- implements ItemAlignmentFacetProvider {
- ItemAlignmentFacet mMultipleFacet;
-
- PositionItemAlignmentFacetProviderForRelativeLayout1() {
- mMultipleFacet = new ItemAlignmentFacet();
- ItemAlignmentFacet.ItemAlignmentDef[] defs =
- new ItemAlignmentFacet.ItemAlignmentDef[2];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t1);
- defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[1].setItemAlignmentViewId(R.id.t2);
- defs[1].setItemAlignmentOffsetPercent(100);
- defs[1].setItemAlignmentOffset(-10);
- mMultipleFacet.setAlignmentDefs(defs);
- }
-
- @Override
- public ItemAlignmentFacet getItemAlignmentFacet(int position) {
- if (position == 0) {
- return mMultipleFacet;
- } else {
- return null;
- }
- }
- }
-
- @Test
- public void testMultipleScrollPosition1() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- TwoViewTypesProvider.class.getName());
- // Set ItemAlignment for each ViewHolder and view type, ViewHolder should
- // override the view type settings.
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
- PositionItemAlignmentFacetProviderForRelativeLayout1.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS,
- ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top",
- mGridView.getPaddingTop(), mGridView.getChildAt(0).getTop());
-
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(mVerifyLayout);
-
- final View v = mGridView.getChildAt(0);
- View t1 = v.findViewById(R.id.t1);
- int t1align = (t1.getTop() + t1.getBottom()) / 2;
- View t2 = v.findViewById(R.id.t2);
- int t2align = t2.getBottom() - 10;
- assertEquals("Expected alignment for 2nd textview",
- mGridView.getPaddingTop() - (t2align - t1align),
- v.getTop());
- }
-
- static class PositionItemAlignmentFacetProviderForRelativeLayout2 implements
- ItemAlignmentFacetProvider {
- ItemAlignmentFacet mMultipleFacet;
-
- PositionItemAlignmentFacetProviderForRelativeLayout2() {
- mMultipleFacet = new ItemAlignmentFacet();
- ItemAlignmentFacet.ItemAlignmentDef[] defs = new ItemAlignmentFacet.ItemAlignmentDef[2];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t1);
- defs[0].setItemAlignmentOffsetPercent(0);
- defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[1].setItemAlignmentViewId(R.id.t2);
- defs[1].setItemAlignmentOffsetPercent(ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- defs[1].setItemAlignmentOffset(-10);
- mMultipleFacet.setAlignmentDefs(defs);
- }
-
- @Override
- public ItemAlignmentFacet getItemAlignmentFacet(int position) {
- if (position == 0) {
- return mMultipleFacet;
- } else {
- return null;
- }
- }
- }
-
- @Test
- public void testMultipleScrollPosition2() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- TwoViewTypesProvider.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
- PositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(mVerifyLayout);
-
- final View v = mGridView.getChildAt(0);
- View t1 = v.findViewById(R.id.t1);
- int t1align = t1.getTop();
- View t2 = v.findViewById(R.id.t2);
- int t2align = t2.getTop() - 10;
- assertEquals("Expected alignment for 2nd textview",
- mGridView.getPaddingTop() - (t2align - t1align), v.getTop());
- }
-
- static class ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2 implements
- ItemAlignmentFacetProvider {
- ItemAlignmentFacet mMultipleFacet;
-
- ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2() {
- mMultipleFacet = new ItemAlignmentFacet();
- ItemAlignmentFacet.ItemAlignmentDef[] defs = new ItemAlignmentFacet.ItemAlignmentDef[2];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t1);
- defs[0].setItemAlignmentOffsetPercent(0);
- defs[1] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[1].setItemAlignmentViewId(R.id.t2);
- defs[1].setItemAlignmentOffsetPercent(100);
- defs[1].setItemAlignmentOffset(-10);
- mMultipleFacet.setAlignmentDefs(defs);
- }
-
- @Override
- public ItemAlignmentFacet getItemAlignmentFacet(int viewType) {
- if (viewType == TwoViewTypesProvider.VIEW_TYPE_FIRST) {
- return mMultipleFacet;
- } else {
- return null;
- }
- }
- }
-
- @Test
- public void testMultipleScrollPosition3() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- TwoViewTypesProvider.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_VIEWTYPE_CLASS,
- ViewTypePositionItemAlignmentFacetProviderForRelativeLayout2.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- waitForScrollIdle(mVerifyLayout);
-
- final View v = mGridView.getChildAt(0);
- View t1 = v.findViewById(R.id.t1);
- int t1align = t1.getTop();
- View t2 = v.findViewById(R.id.t2);
- int t2align = t2.getBottom() - 10;
- assertEquals("Expected alignment for 2nd textview",
- mGridView.getPaddingTop() - (t2align - t1align), v.getTop());
- }
-
- @Test
- public void testSelectionAndAddItemInOneCycle() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(0, new int[]{300, 300});
- mGridView.setSelectedPosition(0);
- }
- });
- assertEquals(0, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testSelectViewTaskSmoothWithAdapterChange() throws Throwable {
- testSelectViewTaskWithAdapterChange(true /*smooth*/);
- }
-
- @Test
- public void testSelectViewTaskWithAdapterChange() throws Throwable {
- testSelectViewTaskWithAdapterChange(false /*smooth*/);
- }
-
- private void testSelectViewTaskWithAdapterChange(final boolean smooth) throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final View firstView = mGridView.getLayoutManager().findViewByPosition(0);
- final View[] selectedViewByTask = new View[1];
- final ViewHolderTask task = new ViewHolderTask() {
- @Override
- public void run(RecyclerView.ViewHolder viewHolder) {
- selectedViewByTask[0] = viewHolder.itemView;
- }
- };
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(0, 1);
- if (smooth) {
- mGridView.setSelectedPositionSmooth(0, task);
- } else {
- mGridView.setSelectedPosition(0, task);
- }
- }
- });
- assertEquals(0, mGridView.getSelectedPosition());
- assertNotNull(selectedViewByTask[0]);
- assertNotSame(firstView, selectedViewByTask[0]);
- assertSame(mGridView.getLayoutManager().findViewByPosition(0), selectedViewByTask[0]);
- }
-
- @Test
- public void testNotifyItemTypeChangedSelectionEvent() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- ChangeableViewTypesProvider.class.getName());
- ChangeableViewTypesProvider.clear();
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final ArrayList<Integer> selectedLog = new ArrayList<Integer>();
- mGridView.setOnChildSelectedListener(new OnChildSelectedListener() {
- @Override
- public void onChildSelected(ViewGroup parent, View view, int position, long id) {
- selectedLog.add(position);
- }
- });
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- ChangeableViewTypesProvider.setViewType(0, 1);
- mGridView.getAdapter().notifyItemChanged(0, 1);
- }
- });
- assertEquals(0, mGridView.getSelectedPosition());
- assertEquals(selectedLog.size(), 1);
- assertEquals((int) selectedLog.get(0), 0);
- }
-
- @Test
- public void testSelectionSmoothAndAddItemInOneCycle() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 0);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.addItems(0, new int[]{300, 300});
- mGridView.setSelectedPositionSmooth(0);
- }
- });
- assertEquals(0, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testExtraLayoutSpace() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
-
- final int windowSize = mGridView.getHeight();
- final int extraLayoutSize = windowSize;
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- // add extra layout space
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setExtraLayoutSpace(extraLayoutSize);
- }
- });
- waitForLayout();
- View v;
- v = mGridView.getChildAt(mGridView.getChildCount() - 1);
- assertTrue(v.getTop() < windowSize + extraLayoutSize);
- assertTrue(v.getBottom() >= windowSize + extraLayoutSize - mGridView.getVerticalMargin());
-
- mGridView.setSelectedPositionSmooth(150);
- waitForScrollIdle(mVerifyLayout);
- v = mGridView.getChildAt(0);
- assertTrue(v.getBottom() > - extraLayoutSize);
- assertTrue(v.getTop() <= -extraLayoutSize + mGridView.getVerticalMargin());
-
- // clear extra layout space
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setExtraLayoutSpace(0);
- verifyMargin();
- }
- });
- Thread.sleep(50);
- v = mGridView.getChildAt(mGridView.getChildCount() - 1);
- assertTrue(v.getTop() < windowSize);
- assertTrue(v.getBottom() >= windowSize - mGridView.getVerticalMargin());
- }
-
- @Test
- public void testFocusFinder() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 3);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- // test focus from button to vertical grid view
- final View button = mActivity.findViewById(R.id.button);
- assertTrue(button.isFocused());
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- assertFalse(mGridView.isFocused());
- assertTrue(mGridView.hasFocus());
-
- // FocusFinder should find last focused(2nd) item on DPAD_DOWN
- final View secondChild = mGridView.getChildAt(1);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- secondChild.requestFocus();
- button.requestFocus();
- }
- });
- assertTrue(button.isFocused());
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- assertTrue(secondChild.isFocused());
-
- // Bug 26918143 Even VerticalGridView is not focusable, FocusFinder should find last focused
- // (2nd) item on DPAD_DOWN.
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- button.requestFocus();
- }
- });
- mGridView.setFocusable(false);
- mGridView.setFocusableInTouchMode(false);
- assertTrue(button.isFocused());
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- assertTrue(secondChild.isFocused());
- }
-
- @Test
- public void testRestoreIndexAndAddItems() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 4);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- assertEquals(mGridView.getSelectedPosition(), 0);
- final SparseArray<Parcelable> states = new SparseArray<>();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.saveHierarchyState(states);
- mGridView.setAdapter(null);
- }
-
- });
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mGridView.restoreHierarchyState(states);
- mActivity.attachToNewAdapter(new int[0]);
- mActivity.addItems(0, new int[]{100, 100, 100, 100});
- }
-
- });
- assertEquals(mGridView.getSelectedPosition(), 0);
- }
-
- @Test
- public void test27766012() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- // set remove animator two seconds
- mGridView.getItemAnimator().setRemoveDuration(2000);
- final View view = mGridView.getChildAt(1);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- view.requestFocus();
- }
- });
- assertTrue(view.hasFocus());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(0, 2);
- }
-
- });
- // wait one second, removing second view is still attached to parent
- Thread.sleep(1000);
- assertSame(view.getParent(), mGridView);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // refocus to the removed item and do a focus search.
- view.requestFocus();
- view.focusSearch(View.FOCUS_UP);
- }
-
- });
- }
-
- @Test
- public void testBug27258366() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- // move item1 500 pixels right, when focus is on item1, default focus finder will pick
- // item0 and item2 for the best match of focusSearch(FOCUS_LEFT). The grid widget
- // must override default addFocusables(), not to add item0 or item2.
- mActivity.mAdapterListener = new GridActivity.AdapterListener() {
- @Override
- public void onBind(RecyclerView.ViewHolder vh, int position) {
- if (position == 1) {
- vh.itemView.setPaddingRelative(500, 0, 0, 0);
- } else {
- vh.itemView.setPaddingRelative(0, 0, 0, 0);
- }
- }
- };
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyDataSetChanged();
- }
- });
- Thread.sleep(100);
-
- final ViewGroup secondChild = (ViewGroup) mGridView.getChildAt(1);
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- secondChild.requestFocus();
- }
- });
- sendKey(KeyEvent.KEYCODE_DPAD_LEFT);
- Thread.sleep(100);
- final View button = mActivity.findViewById(R.id.button);
- assertTrue(button.isFocused());
- }
-
- @Test
- public void testUpdateHeightScrollHorizontal() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, false);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, true);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int childTop = mGridView.getChildAt(0).getTop();
- // scroll to end, all children's top should not change.
- scrollToEnd(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- assertEquals(childTop, mGridView.getChildAt(i).getTop());
- }
- }
- });
- // sanity check last child has focus with a larger height.
- assertTrue(mGridView.getChildAt(0).getHeight()
- < mGridView.getChildAt(mGridView.getChildCount() - 1).getHeight());
- }
-
- @Test
- public void testUpdateWidthScrollHorizontal() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, true);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, false);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int childTop = mGridView.getChildAt(0).getTop();
- // scroll to end, all children's top should not change.
- scrollToEnd(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- assertEquals(childTop, mGridView.getChildAt(i).getTop());
- }
- }
- });
- // sanity check last child has focus with a larger width.
- assertTrue(mGridView.getChildAt(0).getWidth()
- < mGridView.getChildAt(mGridView.getChildCount() - 1).getWidth());
- if (mGridView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
- assertEquals(mGridView.getPaddingLeft(),
- mGridView.getChildAt(mGridView.getChildCount() - 1).getLeft());
- } else {
- assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
- mGridView.getChildAt(mGridView.getChildCount() - 1).getRight());
- }
- }
-
- @Test
- public void testUpdateWidthScrollHorizontalRtl() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear_rtl);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 30);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_REQUEST_LAYOUT_ONFOCUS, true);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE, true);
- intent.putExtra(GridActivity.EXTRA_UPDATE_SIZE_SECONDARY, false);
- initActivity(intent);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- final int childTop = mGridView.getChildAt(0).getTop();
- // scroll to end, all children's top should not change.
- scrollToEnd(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < mGridView.getChildCount(); i++) {
- assertEquals(childTop, mGridView.getChildAt(i).getTop());
- }
- }
- });
- // sanity check last child has focus with a larger width.
- assertTrue(mGridView.getChildAt(0).getWidth()
- < mGridView.getChildAt(mGridView.getChildCount() - 1).getWidth());
- assertEquals(mGridView.getPaddingLeft(),
- mGridView.getChildAt(mGridView.getChildCount() - 1).getLeft());
- }
-
- @Test
- public void testAccessibility() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1000);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- assertTrue(0 == mGridView.getSelectedPosition());
-
- final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
- .getCompatAccessibilityDelegate();
- final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
- }
- });
- assertTrue("test sanity", info.isScrollable());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.performAccessibilityAction(mGridView,
- AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int selectedPosition1 = mGridView.getSelectedPosition();
- assertTrue(0 < selectedPosition1);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
- }
- });
- assertTrue("test sanity", info.isScrollable());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.performAccessibilityAction(mGridView,
- AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- int selectedPosition2 = mGridView.getSelectedPosition();
- assertTrue(selectedPosition2 < selectedPosition1);
- }
-
- @Test
- public void testAccessibilityScrollForwardHalfVisible() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_bottom);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- int height = mGridView.getHeight() - mGridView.getPaddingTop()
- - mGridView.getPaddingBottom();
- final int childHeight = height - mGridView.getVerticalSpacing() - 100;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
- mGridView.setWindowAlignmentOffset(100);
- mGridView.setWindowAlignmentOffsetPercent(BaseGridView
- .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- mGridView.setItemAlignmentOffset(0);
- mGridView.setItemAlignmentOffsetPercent(BaseGridView
- .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- }
- });
- mActivity.addItems(0, new int[]{childHeight, childHeight});
- waitForItemAnimation();
- setSelectedPosition(0);
-
- final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
- .getCompatAccessibilityDelegate();
- final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
- }
- });
- assertTrue("test sanity", info.isScrollable());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.performAccessibilityAction(mGridView,
- AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(1, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testAccessibilityScrollBackwardHalfVisible() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_top);
- intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- int height = mGridView.getHeight() - mGridView.getPaddingTop()
- - mGridView.getPaddingBottom();
- final int childHeight = height - mGridView.getVerticalSpacing() - 100;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
- mGridView.setWindowAlignmentOffset(100);
- mGridView.setWindowAlignmentOffsetPercent(BaseGridView
- .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- mGridView.setItemAlignmentOffset(0);
- mGridView.setItemAlignmentOffsetPercent(BaseGridView
- .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- }
- });
- mActivity.addItems(0, new int[]{childHeight, childHeight});
- waitForItemAnimation();
- setSelectedPosition(1);
-
- final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
- .getCompatAccessibilityDelegate();
- final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
- }
- });
- assertTrue("test sanity", info.isScrollable());
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.performAccessibilityAction(mGridView,
- AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertEquals(0, mGridView.getSelectedPosition());
- }
-
- void slideInAndWaitIdle() throws Throwable {
- slideInAndWaitIdle(5000);
- }
-
- void slideInAndWaitIdle(long timeout) throws Throwable {
- // animateIn() would reset position
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateIn();
- }
- });
- PollingCheck.waitFor(timeout, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return !mGridView.getLayoutManager().isSmoothScrolling()
- && mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- }
-
- @Test
- public void testAnimateOutBlockScrollTo() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- // wait until sliding out.
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
- }
- });
- // scrollToPosition() should not affect slideOut status
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollToPosition(0);
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
- >= mGridView.getHeight());
-
- slideInAndWaitIdle();
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
- }
-
- @Test
- public void testAnimateOutBlockSmoothScrolling() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- int[] items = new int[30];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- // wait until sliding out.
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
- }
- });
- // smoothScrollToPosition() should not affect slideOut status
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.smoothScrollToPosition(29);
- }
- });
- PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
- >= mGridView.getHeight());
-
- slideInAndWaitIdle();
- View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
- assertSame("Scrolled to last child",
- mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
- }
-
- @Test
- public void testAnimateOutBlockLongScrollTo() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- int[] items = new int[30];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- // wait until sliding out.
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
- }
- });
- // smoothScrollToPosition() should not affect slideOut status
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollToPosition(29);
- }
- });
- PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
- >= mGridView.getHeight());
-
- slideInAndWaitIdle();
- View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
- assertSame("Scrolled to last child",
- mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
- }
-
- @Test
- public void testAnimateOutBlockLayout() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- // wait until sliding out.
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
- }
- });
- // change adapter should not affect slideOut status
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mActivity.changeItem(0, 200);
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
- >= mGridView.getHeight());
- assertEquals("onLayout suppressed during slide out", 300,
- mGridView.getChildAt(0).getHeight());
-
- slideInAndWaitIdle();
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
- // size of item should be updated immediately after slide in animation finishes:
- PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return 200 == mGridView.getChildAt(0).getHeight();
- }
- });
- }
-
- @Test
- public void testAnimateOutBlockFocusChange() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- mActivity.findViewById(R.id.button).requestFocus();
- }
- });
- assertTrue(mActivity.findViewById(R.id.button).hasFocus());
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.requestFocus();
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
- assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
- >= mGridView.getHeight());
-
- slideInAndWaitIdle();
- assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
- mGridView.getChildAt(0).getTop());
- }
-
- @Test
- public void testHorizontalAnimateOutBlockScrollTo() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
- mGridView.getChildAt(0).getLeft());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getLeft() > mGridView.getPaddingLeft();
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.scrollToPosition(0);
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
-
- assertTrue("First view is slided out", mGridView.getChildAt(0).getLeft()
- > mGridView.getWidth());
-
- slideInAndWaitIdle();
- assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
- mGridView.getChildAt(0).getLeft());
-
- }
-
- @Test
- public void testHorizontalAnimateOutRtl() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.horizontal_linear_rtl);
- int[] items = new int[100];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- assertEquals("First view is aligned with padding right",
- mGridView.getWidth() - mGridView.getPaddingRight(),
- mGridView.getChildAt(0).getRight());
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.animateOut();
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getChildAt(0).getRight()
- < mGridView.getWidth() - mGridView.getPaddingRight();
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.smoothScrollToPosition(0);
- }
- });
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
- }
- });
-
- assertTrue("First view is slided out", mGridView.getChildAt(0).getRight() < 0);
-
- slideInAndWaitIdle();
- assertEquals("First view is aligned with padding right",
- mGridView.getWidth() - mGridView.getPaddingRight(),
- mGridView.getChildAt(0).getRight());
- }
-
- @Test
- public void testSmoothScrollerOutRange() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
- R.layout.vertical_linear_with_button_onleft);
- intent.putExtra(GridActivity.EXTRA_REQUEST_FOCUS_ONLAYOUT, true);
- int[] items = new int[30];
- for (int i = 0; i < items.length; i++) {
- items[i] = 680;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- final View button = mActivity.findViewById(R.id.button);
- mActivityTestRule.runOnUiThread(new Runnable() {
- public void run() {
- button.requestFocus();
- }
- });
-
- mGridView.setSelectedPositionSmooth(0);
- waitForScrollIdle(mVerifyLayout);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- public void run() {
- mGridView.setSelectedPositionSmooth(120);
- }
- });
- waitForScrollIdle(mVerifyLayout);
- assertTrue(button.hasFocus());
- int key;
- if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
- key = KeyEvent.KEYCODE_DPAD_LEFT;
- } else {
- key = KeyEvent.KEYCODE_DPAD_RIGHT;
- }
- sendKey(key);
- // the GridView should has focus in its children
- assertTrue(mGridView.hasFocus());
- assertFalse(mGridView.isFocused());
- assertEquals(29, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testRemoveLastItemWithStableId() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, true);
- int[] items = new int[1];
- for (int i = 0; i < items.length; i++) {
- items[i] = 680;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setRemoveDuration(2000);
- mActivity.removeItems(0, 1, false);
- mGridView.getAdapter().notifyDataSetChanged();
- }
- });
- Thread.sleep(500);
- assertEquals(-1, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testUpdateAndSelect1() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyDataSetChanged();
- mGridView.setSelectedPosition(1);
- }
- });
- waitOneUiCycle();
- assertEquals(1, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testUpdateAndSelect2() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getAdapter().notifyDataSetChanged();
- mGridView.setSelectedPosition(50);
- }
- });
- waitOneUiCycle();
- assertEquals(50, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testUpdateAndSelect3() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_HAS_STABLE_IDS, false);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- int[] newItems = new int[100];
- for (int i = 0; i < newItems.length; i++) {
- newItems[i] = mActivity.mItemLengths[0];
- }
- mActivity.addItems(0, newItems, false);
- mGridView.getAdapter().notifyDataSetChanged();
- mGridView.setSelectedPosition(50);
- }
- });
- waitOneUiCycle();
- assertEquals(50, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testFocusedPositonAfterRemoved1() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- final int[] items = new int[2];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- setSelectedPosition(1);
- assertEquals(1, mGridView.getSelectedPosition());
-
- final int[] newItems = new int[3];
- for (int i = 0; i < newItems.length; i++) {
- newItems[i] = 300;
- }
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(0, 2, true);
- mActivity.addItems(0, newItems, true);
- }
- });
- assertEquals(0, mGridView.getSelectedPosition());
- }
-
- @Test
- public void testFocusedPositonAfterRemoved2() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- final int[] items = new int[2];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- setSelectedPosition(1);
- assertEquals(1, mGridView.getSelectedPosition());
-
- final int[] newItems = new int[3];
- for (int i = 0; i < newItems.length; i++) {
- newItems[i] = 300;
- }
- performAndWaitForAnimation(new Runnable() {
- @Override
- public void run() {
- mActivity.removeItems(1, 1, true);
- mActivity.addItems(1, newItems, true);
- }
- });
- assertEquals(1, mGridView.getSelectedPosition());
- }
-
- static void assertNoCollectionItemInfo(AccessibilityNodeInfoCompat info) {
- AccessibilityNodeInfoCompat.CollectionItemInfoCompat nodeInfoCompat =
- info.getCollectionItemInfo();
- if (nodeInfoCompat == null) {
- return;
- }
- assertTrue(nodeInfoCompat.getRowIndex() < 0);
- assertTrue(nodeInfoCompat.getColumnIndex() < 0);
- }
-
- /**
- * This test would need talkback on.
- */
- @Test
- public void testAccessibilityOfItemsBeingPushedOut() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- final int lastPos = mGridView.getChildAdapterPosition(
- mGridView.getChildAt(mGridView.getChildCount() - 1));
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getLayoutManager().setItemPrefetchEnabled(false);
- }
- });
- final int numItemsToPushOut = mNumRows;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- // set longer enough so that accessibility service will initialize node
- // within setImportantForAccessibility().
- mGridView.getItemAnimator().setRemoveDuration(2000);
- mGridView.getItemAnimator().setAddDuration(2000);
- final int[] newItems = new int[numItemsToPushOut];
- final int newItemValue = mActivity.mItemLengths[0];
- for (int i = 0; i < newItems.length; i++) {
- newItems[i] = newItemValue;
- }
- mActivity.addItems(lastPos - numItemsToPushOut + 1, newItems);
- }
- });
- waitForItemAnimation();
- }
-
- /**
- * This test simulates talkback by calling setImportanceForAccessibility at end of animation
- */
- @Test
- public void simulatesAccessibilityOfItemsBeingPushedOut() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- final HashSet<View> moveAnimationViews = new HashSet();
- mActivity.mImportantForAccessibilityListener =
- new GridActivity.ImportantForAccessibilityListener() {
- RecyclerView.LayoutManager mLM = mGridView.getLayoutManager();
- @Override
- public void onImportantForAccessibilityChanged(View view, int newValue) {
- // simulates talkack, having setImportantForAccessibility to call
- // onInitializeAccessibilityNodeInfoForItem() for the DISAPPEARING items.
- if (moveAnimationViews.contains(view)) {
- AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
- mLM.onInitializeAccessibilityNodeInfoForItem(
- null, null, view, info);
- }
- }
- };
- final int lastPos = mGridView.getChildAdapterPosition(
- mGridView.getChildAt(mGridView.getChildCount() - 1));
- final int numItemsToPushOut = mNumRows;
- for (int i = 0; i < numItemsToPushOut; i++) {
- moveAnimationViews.add(
- mGridView.getChildAt(mGridView.getChildCount() - 1 - i));
- }
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setItemAnimator(new DefaultItemAnimator() {
- @Override
- public void onMoveFinished(RecyclerView.ViewHolder item) {
- moveAnimationViews.remove(item.itemView);
- }
- });
- mGridView.getLayoutManager().setItemPrefetchEnabled(false);
- }
- });
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final int[] newItems = new int[numItemsToPushOut];
- final int newItemValue = mActivity.mItemLengths[0] + 1;
- for (int i = 0; i < newItems.length; i++) {
- newItems[i] = newItemValue;
- }
- mActivity.addItems(lastPos - numItemsToPushOut + 1, newItems);
- }
- });
- while (moveAnimationViews.size() != 0) {
- Thread.sleep(100);
- }
- }
-
- @Test
- public void testAccessibilityNodeInfoOnRemovedFirstItem() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 6);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- final View lastView = mGridView.findViewHolderForAdapterPosition(0).itemView;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setRemoveDuration(20000);
- mActivity.removeItems(0, 1);
- }
- });
- waitForItemAnimationStart();
- AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(lastView);
- mGridView.getLayoutManager().onInitializeAccessibilityNodeInfoForItem(null, null,
- lastView, info);
- assertNoCollectionItemInfo(info);
- }
-
- @Test
- public void testAccessibilityNodeInfoOnRemovedLastItem() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 6);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 3;
-
- initActivity(intent);
-
- final View lastView = mGridView.findViewHolderForAdapterPosition(5).itemView;
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.getItemAnimator().setRemoveDuration(20000);
- mActivity.removeItems(5, 1);
- }
- });
- waitForItemAnimationStart();
- AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(lastView);
- mGridView.getLayoutManager().onInitializeAccessibilityNodeInfoForItem(null, null,
- lastView, info);
- assertNoCollectionItemInfo(info);
- }
-
- static class FiveViewTypesProvider implements ViewTypeProvider {
-
- @Override
- public int getViewType(int position) {
- switch (position) {
- case 0:
- return 0;
- case 1:
- return 1;
- case 2:
- return 2;
- case 3:
- return 3;
- case 4:
- return 4;
- }
- return 199;
- }
- }
-
- // Used by testItemAlignmentVertical() testItemAlignmentHorizontal()
- static class ItemAlignmentWithPaddingFacetProvider implements
- ItemAlignmentFacetProvider {
- final ItemAlignmentFacet mFacet0;
- final ItemAlignmentFacet mFacet1;
- final ItemAlignmentFacet mFacet2;
- final ItemAlignmentFacet mFacet3;
- final ItemAlignmentFacet mFacet4;
-
- ItemAlignmentWithPaddingFacetProvider() {
- ItemAlignmentFacet.ItemAlignmentDef[] defs;
- mFacet0 = new ItemAlignmentFacet();
- defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t1);
- defs[0].setItemAlignmentOffsetPercent(0);
- defs[0].setItemAlignmentOffsetWithPadding(false);
- mFacet0.setAlignmentDefs(defs);
- mFacet1 = new ItemAlignmentFacet();
- defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t1);
- defs[0].setItemAlignmentOffsetPercent(0);
- defs[0].setItemAlignmentOffsetWithPadding(true);
- mFacet1.setAlignmentDefs(defs);
- mFacet2 = new ItemAlignmentFacet();
- defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t2);
- defs[0].setItemAlignmentOffsetPercent(100);
- defs[0].setItemAlignmentOffsetWithPadding(true);
- mFacet2.setAlignmentDefs(defs);
- mFacet3 = new ItemAlignmentFacet();
- defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t2);
- defs[0].setItemAlignmentOffsetPercent(50);
- defs[0].setItemAlignmentOffsetWithPadding(true);
- mFacet3.setAlignmentDefs(defs);
- mFacet4 = new ItemAlignmentFacet();
- defs = new ItemAlignmentFacet.ItemAlignmentDef[1];
- defs[0] = new ItemAlignmentFacet.ItemAlignmentDef();
- defs[0].setItemAlignmentViewId(R.id.t2);
- defs[0].setItemAlignmentOffsetPercent(50);
- defs[0].setItemAlignmentOffsetWithPadding(false);
- mFacet4.setAlignmentDefs(defs);
- }
-
- @Override
- public ItemAlignmentFacet getItemAlignmentFacet(int viewType) {
- switch (viewType) {
- case 0:
- return mFacet0;
- case 1:
- return mFacet1;
- case 2:
- return mFacet2;
- case 3:
- return mFacet3;
- case 4:
- return mFacet4;
- }
- return null;
- }
- }
-
- @Test
- public void testItemAlignmentVertical() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout2);
- int[] items = new int[5];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- FiveViewTypesProvider.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
- ItemAlignmentWithPaddingFacetProvider.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
- mGridView.setWindowAlignmentOffsetPercent(50);
- mGridView.setWindowAlignmentOffset(0);
- }
- });
- waitForLayout();
-
- final float windowAlignCenter = mGridView.getHeight() / 2f;
- Rect rect = new Rect();
- View textView;
-
- // test 1: does not include padding
- textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
- rect.set(0, 0, textView.getWidth(), textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.top, DELTA);
-
- // test 2: including low padding
- setSelectedPosition(1);
- textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
- assertTrue(textView.getPaddingTop() > 0);
- rect.set(0, textView.getPaddingTop(), textView.getWidth(), textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.top, DELTA);
-
- // test 3: including high padding
- setSelectedPosition(2);
- textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingBottom() > 0);
- rect.set(0, 0, textView.getWidth(),
- textView.getHeight() - textView.getPaddingBottom());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.bottom, DELTA);
-
- // test 4: including padding will be ignored if offsetPercent is not 0 or 100
- setSelectedPosition(3);
- textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingTop() != textView.getPaddingBottom());
- rect.set(0, 0, textView.getWidth(), textView.getHeight() / 2);
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.bottom, DELTA);
-
- // test 5: does not include padding
- setSelectedPosition(4);
- textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingTop() != textView.getPaddingBottom());
- rect.set(0, 0, textView.getWidth(), textView.getHeight() / 2);
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.bottom, DELTA);
- }
-
- @Test
- public void testItemAlignmentHorizontal() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout3);
- int[] items = new int[5];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- FiveViewTypesProvider.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
- ItemAlignmentWithPaddingFacetProvider.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
- mGridView.setWindowAlignmentOffsetPercent(50);
- mGridView.setWindowAlignmentOffset(0);
- }
- });
- waitForLayout();
-
- final float windowAlignCenter = mGridView.getWidth() / 2f;
- Rect rect = new Rect();
- View textView;
-
- // test 1: does not include padding
- textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
- rect.set(0, 0, textView.getWidth(), textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.left, DELTA);
-
- // test 2: including low padding
- setSelectedPosition(1);
- textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
- assertTrue(textView.getPaddingLeft() > 0);
- rect.set(textView.getPaddingLeft(), 0, textView.getWidth(), textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.left, DELTA);
-
- // test 3: including high padding
- setSelectedPosition(2);
- textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingRight() > 0);
- rect.set(0, 0, textView.getWidth() - textView.getPaddingRight(),
- textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
-
- // test 4: including padding will be ignored if offsetPercent is not 0 or 100
- setSelectedPosition(3);
- textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
- rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
-
- // test 5: does not include padding
- setSelectedPosition(4);
- textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
- rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
- }
-
- @Test
- public void testItemAlignmentHorizontalRtl() throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.relative_layout3);
- int[] items = new int[5];
- for (int i = 0; i < items.length; i++) {
- items[i] = 300;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- intent.putExtra(GridActivity.EXTRA_VIEWTYPEPROVIDER_CLASS,
- FiveViewTypesProvider.class.getName());
- intent.putExtra(GridActivity.EXTRA_ITEMALIGNMENTPROVIDER_CLASS,
- ItemAlignmentWithPaddingFacetProvider.class.getName());
- mOrientation = BaseGridView.VERTICAL;
- mNumRows = 1;
-
- initActivity(intent);
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
- mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
- mGridView.setWindowAlignmentOffsetPercent(50);
- mGridView.setWindowAlignmentOffset(0);
- }
- });
- waitForLayout();
-
- final float windowAlignCenter = mGridView.getWidth() / 2f;
- Rect rect = new Rect();
- View textView;
-
- // test 1: does not include padding
- textView = mGridView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.t1);
- rect.set(0, 0, textView.getWidth(), textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
-
- // test 2: including low padding
- setSelectedPosition(1);
- textView = mGridView.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.t1);
- assertTrue(textView.getPaddingRight() > 0);
- rect.set(0, 0, textView.getWidth() - textView.getPaddingRight(),
- textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
-
- // test 3: including high padding
- setSelectedPosition(2);
- textView = mGridView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingLeft() > 0);
- rect.set(textView.getPaddingLeft(), 0, textView.getWidth(),
- textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.left, DELTA);
-
- // test 4: including padding will be ignored if offsetPercent is not 0 or 100
- setSelectedPosition(3);
- textView = mGridView.findViewHolderForAdapterPosition(3).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
- rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
-
- // test 5: does not include padding
- setSelectedPosition(4);
- textView = mGridView.findViewHolderForAdapterPosition(4).itemView.findViewById(R.id.t2);
- assertTrue(textView.getPaddingLeft() != textView.getPaddingRight());
- rect.set(0, 0, textView.getWidth() / 2, textView.getHeight());
- mGridView.offsetDescendantRectToMyCoords(textView, rect);
- assertEquals(windowAlignCenter, rect.right, DELTA);
- }
-
- enum ItemLocation {
- ITEM_AT_LOW,
- ITEM_AT_KEY_LINE,
- ITEM_AT_HIGH
- };
-
- static class ItemAt {
- final int mScrollPosition;
- final int mPosition;
- final ItemLocation mLocation;
-
- ItemAt(int scrollPosition, int position, ItemLocation loc) {
- mScrollPosition = scrollPosition;
- mPosition = position;
- mLocation = loc;
- }
-
- ItemAt(int position, ItemLocation loc) {
- mScrollPosition = position;
- mPosition = position;
- mLocation = loc;
- }
- }
-
- /**
- * When scroll to position, item at position is expected at given location.
- */
- static ItemAt itemAt(int position, ItemLocation location) {
- return new ItemAt(position, location);
- }
-
- /**
- * When scroll to scrollPosition, item at position is expected at given location.
- */
- static ItemAt itemAt(int scrollPosition, int position, ItemLocation location) {
- return new ItemAt(scrollPosition, position, location);
- }
-
- void prepareKeyLineTest(int numItems) throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
- int[] items = new int[numItems];
- for (int i = 0; i < items.length; i++) {
- items[i] = 32;
- }
- intent.putExtra(GridActivity.EXTRA_ITEMS, items);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.HORIZONTAL;
- mNumRows = 1;
-
- initActivity(intent);
- }
-
- public void testPreferKeyLine(final int windowAlignment,
- final boolean preferKeyLineOverLow,
- final boolean preferKeyLineOverHigh,
- ItemLocation assertFirstItemLocation,
- ItemLocation assertLastItemLocation) throws Throwable {
- testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
- itemAt(0, assertFirstItemLocation),
- itemAt(mActivity.mNumItems - 1, assertLastItemLocation));
- }
-
- public void testPreferKeyLine(final int windowAlignment,
- final boolean preferKeyLineOverLow,
- final boolean preferKeyLineOverHigh,
- ItemLocation assertFirstItemLocation,
- ItemAt assertLastItemLocation) throws Throwable {
- testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
- itemAt(0, assertFirstItemLocation),
- assertLastItemLocation);
- }
-
- public void testPreferKeyLine(final int windowAlignment,
- final boolean preferKeyLineOverLow,
- final boolean preferKeyLineOverHigh,
- ItemAt assertFirstItemLocation,
- ItemLocation assertLastItemLocation) throws Throwable {
- testPreferKeyLine(windowAlignment, preferKeyLineOverLow, preferKeyLineOverHigh,
- assertFirstItemLocation,
- itemAt(mActivity.mNumItems - 1, assertLastItemLocation));
- }
-
- public void testPreferKeyLine(final int windowAlignment,
- final boolean preferKeyLineOverLow,
- final boolean preferKeyLineOverHigh,
- ItemAt assertFirstItemLocation,
- ItemAt assertLastItemLocation) throws Throwable {
- TestPreferKeyLineOptions options = new TestPreferKeyLineOptions();
- options.mAssertItemLocations = new ItemAt[] {assertFirstItemLocation,
- assertLastItemLocation};
- options.mPreferKeyLineOverLow = preferKeyLineOverLow;
- options.mPreferKeyLineOverHigh = preferKeyLineOverHigh;
- options.mWindowAlignment = windowAlignment;
-
- options.mRtl = false;
- testPreferKeyLine(options);
-
- options.mRtl = true;
- testPreferKeyLine(options);
- }
-
- static class TestPreferKeyLineOptions {
- int mWindowAlignment;
- boolean mPreferKeyLineOverLow;
- boolean mPreferKeyLineOverHigh;
- ItemAt[] mAssertItemLocations;
- boolean mRtl;
- }
-
- public void testPreferKeyLine(final TestPreferKeyLineOptions options) throws Throwable {
- startWaitLayout();
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (options.mRtl) {
- mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
- } else {
- mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
- }
- mGridView.setWindowAlignment(options.mWindowAlignment);
- mGridView.setWindowAlignmentOffsetPercent(50);
- mGridView.setWindowAlignmentOffset(0);
- mGridView.setWindowAlignmentPreferKeyLineOverLowEdge(options.mPreferKeyLineOverLow);
- mGridView.setWindowAlignmentPreferKeyLineOverHighEdge(
- options.mPreferKeyLineOverHigh);
- }
- });
- waitForLayout();
-
- final int paddingStart = mGridView.getPaddingStart();
- final int paddingEnd = mGridView.getPaddingEnd();
- final int windowAlignCenter = mGridView.getWidth() / 2;
-
- for (int i = 0; i < options.mAssertItemLocations.length; i++) {
- ItemAt assertItemLocation = options.mAssertItemLocations[i];
- setSelectedPosition(assertItemLocation.mScrollPosition);
- View view = mGridView.findViewHolderForAdapterPosition(assertItemLocation.mPosition)
- .itemView;
- switch (assertItemLocation.mLocation) {
- case ITEM_AT_LOW:
- if (options.mRtl) {
- assertEquals(mGridView.getWidth() - paddingStart, view.getRight());
- } else {
- assertEquals(paddingStart, view.getLeft());
- }
- break;
- case ITEM_AT_HIGH:
- if (options.mRtl) {
- assertEquals(paddingEnd, view.getLeft());
- } else {
- assertEquals(mGridView.getWidth() - paddingEnd, view.getRight());
- }
- break;
- case ITEM_AT_KEY_LINE:
- assertEquals(windowAlignCenter, (view.getLeft() + view.getRight()) / 2, DELTA);
- break;
- }
- }
- }
-
- @Test
- public void testPreferKeyLine1() throws Throwable {
- prepareKeyLineTest(1);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
- ItemLocation.ITEM_AT_HIGH, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
- ItemLocation.ITEM_AT_HIGH, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_LOW);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- }
-
- @Test
- public void testPreferKeyLine2() throws Throwable {
- prepareKeyLineTest(2);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
- itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
- itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
- itemAt(0, 1, ItemLocation.ITEM_AT_HIGH),
- itemAt(1, 1, ItemLocation.ITEM_AT_HIGH));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
- itemAt(0, 0, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 0, ItemLocation.ITEM_AT_KEY_LINE));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
- itemAt(0, 1, ItemLocation.ITEM_AT_HIGH),
- itemAt(1, 1, ItemLocation.ITEM_AT_HIGH));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
- itemAt(0, 0, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 0, ItemLocation.ITEM_AT_KEY_LINE));
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, itemAt(1, 0, ItemLocation.ITEM_AT_LOW));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
- itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
- itemAt(0, 1, ItemLocation.ITEM_AT_KEY_LINE),
- itemAt(1, 1, ItemLocation.ITEM_AT_KEY_LINE));
- }
-
- @Test
- public void testPreferKeyLine10000() throws Throwable {
- prepareKeyLineTest(10000);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, false, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_NO_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_LOW_EDGE, true, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_KEY_LINE);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, false, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, false,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE, true, true,
- ItemLocation.ITEM_AT_KEY_LINE, ItemLocation.ITEM_AT_HIGH);
-
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, false, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, false,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
- testPreferKeyLine(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE, true, true,
- ItemLocation.ITEM_AT_LOW, ItemLocation.ITEM_AT_HIGH);
- }
-}
diff --git a/v4/Android.mk b/v4/Android.mk
index a9c9145..84fd5c3 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -27,7 +27,7 @@
LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
# Some projects expect to inherit android-support-annotations from
# android-support-v4, so we need to keep it static until they can be fixed.
-LOCAL_STATIC_JAVA_LIBRARIES := \
+LOCAL_STATIC_ANDROID_LIBRARIES := \
android-support-compat \
android-support-media-compat \
android-support-core-utils \
diff --git a/v7/appcompat/api/current.txt b/v7/appcompat/api/current.txt
index 93d0186..e78aece 100644
--- a/v7/appcompat/api/current.txt
+++ b/v7/appcompat/api/current.txt
@@ -303,6 +303,24 @@
ctor public AppCompatDialogFragment();
}
+ public class AppCompatViewInflater {
+ ctor public AppCompatViewInflater();
+ method protected android.support.v7.widget.AppCompatAutoCompleteTextView createAutoCompleteTextView(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatButton createButton(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatCheckBox createCheckBox(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatCheckedTextView createCheckedTextView(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatEditText createEditText(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatImageButton createImageButton(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatImageView createImageView(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatRadioButton createRadioButton(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatRatingBar createRatingBar(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatSeekBar createSeekBar(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatSpinner createSpinner(android.content.Context, android.util.AttributeSet);
+ method protected android.support.v7.widget.AppCompatTextView createTextView(android.content.Context, android.util.AttributeSet);
+ method protected android.view.View createView(android.content.Context, java.lang.String, android.util.AttributeSet);
+ }
+
}
package android.support.v7.content.res {
@@ -385,6 +403,15 @@
method public abstract void onActionViewExpanded();
}
+ public class ContextThemeWrapper extends android.content.ContextWrapper {
+ ctor public ContextThemeWrapper();
+ ctor public ContextThemeWrapper(android.content.Context, int);
+ ctor public ContextThemeWrapper(android.content.Context, android.content.res.Resources.Theme);
+ method public void applyOverrideConfiguration(android.content.res.Configuration);
+ method public int getThemeResId();
+ method protected void onApplyThemeResource(android.content.res.Resources.Theme, int, boolean);
+ }
+
}
package android.support.v7.widget {
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index 308a122..8e242cc 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -16,7 +16,9 @@
androidTestImplementation libs.espresso_core, { exclude module: 'support-annotations' }
androidTestImplementation libs.mockito_core, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
- androidTestImplementation project(':support-testutils')
+ androidTestImplementation project(':support-testutils'), {
+ exclude group: 'com.android.support', module: 'appcompat-v7'
+ }
}
android {
diff --git a/v7/appcompat/res/anim/tooltip_enter.xml b/v7/appcompat/res/anim/abc_tooltip_enter.xml
similarity index 100%
rename from v7/appcompat/res/anim/tooltip_enter.xml
rename to v7/appcompat/res/anim/abc_tooltip_enter.xml
diff --git a/v7/appcompat/res/anim/tooltip_exit.xml b/v7/appcompat/res/anim/abc_tooltip_exit.xml
similarity index 100%
rename from v7/appcompat/res/anim/tooltip_exit.xml
rename to v7/appcompat/res/anim/abc_tooltip_exit.xml
diff --git a/v7/appcompat/res/drawable-watch/abc_dialog_material_background.xml b/v7/appcompat/res/drawable-watch/abc_dialog_material_background.xml
new file mode 100644
index 0000000..242761b
--- /dev/null
+++ b/v7/appcompat/res/drawable-watch/abc_dialog_material_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/white" />
+</shape>
diff --git a/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml b/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml
new file mode 100644
index 0000000..1c8bd93
--- /dev/null
+++ b/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<android.support.v7.widget.ButtonBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="bottom"
+ android:layoutDirection="locale"
+ android:orientation="horizontal"
+ android:paddingBottom="4dp"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:paddingTop="4dp">
+
+ <Button
+ android:id="@android:id/button3"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+ <Button
+ android:id="@android:id/button2"
+ style="?attr/buttonBarNegativeButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+ <Button
+ android:id="@android:id/button1"
+ style="?attr/buttonBarPositiveButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+</android.support.v7.widget.ButtonBarLayout>
diff --git a/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml b/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml
new file mode 100644
index 0000000..e100963
--- /dev/null
+++ b/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/topPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="top|center_horizontal">
+
+ <!-- If the client uses a customTitle, it will be added here. -->
+
+ <LinearLayout
+ android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginTop="24dp"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:adjustViewBounds="true"
+ android:maxHeight="24dp"
+ android:maxWidth="24dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <android.support.v7.widget.DialogTitle
+ android:id="@+id/alertTitle"
+ style="?android:attr/windowTitleStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center" />
+
+ </LinearLayout>
+
+ <android.support.v4.widget.Space
+ android:id="@+id/titleDividerNoCustom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/abc_dialog_title_divider_material"
+ android:visibility="gone"/>
+</LinearLayout>
diff --git a/v7/appcompat/res/layout/tooltip.xml b/v7/appcompat/res/layout/abc_tooltip.xml
similarity index 100%
rename from v7/appcompat/res/layout/tooltip.xml
rename to v7/appcompat/res/layout/abc_tooltip.xml
diff --git a/v7/appcompat/res/values-watch/themes_base.xml b/v7/appcompat/res/values-watch/themes_base.xml
new file mode 100644
index 0000000..20d8a7b
--- /dev/null
+++ b/v7/appcompat/res/values-watch/themes_base.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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>
+ <style name="Base.Theme.AppCompat.Dialog" parent="Base.V7.Theme.AppCompat.Dialog" >
+ <item name="android:windowIsFloating">false</item>
+ </style>
+ <style name="Base.Theme.AppCompat.Light.Dialog" parent="Base.V7.Theme.AppCompat.Light.Dialog" >
+ <item name="android:windowIsFloating">false</item>
+ </style>
+ <style name="Base.ThemeOverlay.AppCompat.Dialog" parent="Base.V7.ThemeOverlay.AppCompat.Dialog" >
+ <item name="android:windowIsFloating">false</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 52ae694..2012a3a 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -407,6 +407,8 @@
<!-- Color used for error states and things that need to be drawn to
the user's attention. -->
<attr name="colorError" format="reference|color" />
+
+ <attr name="viewInflaterClass" format="string" />
</declare-styleable>
diff --git a/v7/appcompat/res/values/styles_base.xml b/v7/appcompat/res/values/styles_base.xml
index 2b2db12..adaaae0 100644
--- a/v7/appcompat/res/values/styles_base.xml
+++ b/v7/appcompat/res/values/styles_base.xml
@@ -525,8 +525,8 @@
<style name="Base.AlertDialog.AppCompat.Light" parent="Base.AlertDialog.AppCompat" />
<style name="Base.Animation.AppCompat.Tooltip" parent="android:Animation">
- <item name="android:windowEnterAnimation">@anim/tooltip_enter</item>
- <item name="android:windowExitAnimation">@anim/tooltip_exit</item>
+ <item name="android:windowEnterAnimation">@anim/abc_tooltip_enter</item>
+ <item name="android:windowExitAnimation">@anim/abc_tooltip_exit</item>
</style>
</resources>
diff --git a/v7/appcompat/res/values/themes_base.xml b/v7/appcompat/res/values/themes_base.xml
index a5be8ad..a9acfce 100644
--- a/v7/appcompat/res/values/themes_base.xml
+++ b/v7/appcompat/res/values/themes_base.xml
@@ -119,6 +119,7 @@
<!-- Base platform-dependent theme providing an action bar in a dark-themed activity. -->
<style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat">
+ <item name="viewInflaterClass">android.support.v7.app.AppCompatViewInflater</item>
<item name="windowNoTitle">false</item>
<item name="windowActionBar">true</item>
<item name="windowActionBarOverlay">false</item>
@@ -287,6 +288,8 @@
<!-- Base platform-dependent theme providing an action bar in a light-themed activity. -->
<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
+ <item name="viewInflaterClass">android.support.v7.app.AppCompatViewInflater</item>
+
<item name="windowNoTitle">false</item>
<item name="windowActionBar">true</item>
<item name="windowActionBarOverlay">false</item>
diff --git a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
index 056e33e..5b53401 100644
--- a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
+++ b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
@@ -1001,7 +1001,26 @@
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
- mAppCompatViewInflater = new AppCompatViewInflater();
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
+ String viewInflaterClassName =
+ a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
+ if ((viewInflaterClassName == null)
+ || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
+ // Either default class name or set explicitly to null. In both cases
+ // create the base inflater (no reflection)
+ mAppCompatViewInflater = new AppCompatViewInflater();
+ } else {
+ try {
+ Class viewInflaterClass = Class.forName(viewInflaterClassName);
+ mAppCompatViewInflater =
+ (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
+ .newInstance();
+ } catch (Throwable t) {
+ Log.i(TAG, "Failed to instantiate custom view inflater "
+ + viewInflaterClassName + ". Falling back to default.", t);
+ mAppCompatViewInflater = new AppCompatViewInflater();
+ }
+ }
}
boolean inheritContext = false;
diff --git a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
index 54d01bc..87a1a3c 100644
--- a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
+++ b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
@@ -51,14 +51,12 @@
import java.util.Map;
/**
- * This class is responsible for manually inflating our tinted widgets which are used on devices
- * running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class
- * should only be used when running on those devices.
+ * This class is responsible for manually inflating our tinted widgets.
* <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
* the framework versions in layout inflation; the second is backport the {@code android:theme}
* functionality for any inflated widgets. This include theme inheritance from its parent.
*/
-class AppCompatViewInflater {
+public class AppCompatViewInflater {
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
@@ -77,7 +75,7 @@
private final Object[] mConstructorArgs = new Object[2];
- public final View createView(View parent, final String name, @NonNull Context context,
+ final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
@@ -100,44 +98,63 @@
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
- view = new AppCompatTextView(context, attrs);
+ view = createTextView(context, attrs);
+ verifyNotNull(view, name);
break;
case "ImageView":
- view = new AppCompatImageView(context, attrs);
+ view = createImageView(context, attrs);
+ verifyNotNull(view, name);
break;
case "Button":
- view = new AppCompatButton(context, attrs);
+ view = createButton(context, attrs);
+ verifyNotNull(view, name);
break;
case "EditText":
- view = new AppCompatEditText(context, attrs);
+ view = createEditText(context, attrs);
+ verifyNotNull(view, name);
break;
case "Spinner":
- view = new AppCompatSpinner(context, attrs);
+ view = createSpinner(context, attrs);
+ verifyNotNull(view, name);
break;
case "ImageButton":
- view = new AppCompatImageButton(context, attrs);
+ view = createImageButton(context, attrs);
+ verifyNotNull(view, name);
break;
case "CheckBox":
- view = new AppCompatCheckBox(context, attrs);
+ view = createCheckBox(context, attrs);
+ verifyNotNull(view, name);
break;
case "RadioButton":
- view = new AppCompatRadioButton(context, attrs);
+ view = createRadioButton(context, attrs);
+ verifyNotNull(view, name);
break;
case "CheckedTextView":
- view = new AppCompatCheckedTextView(context, attrs);
+ view = createCheckedTextView(context, attrs);
+ verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
- view = new AppCompatAutoCompleteTextView(context, attrs);
+ view = createAutoCompleteTextView(context, attrs);
+ verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
- view = new AppCompatMultiAutoCompleteTextView(context, attrs);
+ view = createMultiAutoCompleteTextView(context, attrs);
+ verifyNotNull(view, name);
break;
case "RatingBar":
- view = new AppCompatRatingBar(context, attrs);
+ view = createRatingBar(context, attrs);
+ verifyNotNull(view, name);
break;
case "SeekBar":
- view = new AppCompatSeekBar(context, attrs);
+ view = createSeekBar(context, attrs);
+ verifyNotNull(view, name);
break;
+ default:
+ // The fallback that allows extending class to take over view inflation
+ // for other tags. Note that we don't check that the result is not-null.
+ // That allows the custom inflater path to fall back on the default one
+ // later in this method.
+ view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
@@ -154,6 +171,85 @@
return view;
}
+ @NonNull
+ protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
+ return new AppCompatTextView(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
+ return new AppCompatImageView(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+ return new AppCompatButton(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatEditText createEditText(Context context, AttributeSet attrs) {
+ return new AppCompatEditText(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) {
+ return new AppCompatSpinner(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
+ return new AppCompatImageButton(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) {
+ return new AppCompatCheckBox(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
+ return new AppCompatRadioButton(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) {
+ return new AppCompatCheckedTextView(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatAutoCompleteTextView createAutoCompleteTextView(Context context,
+ AttributeSet attrs) {
+ return new AppCompatAutoCompleteTextView(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(Context context,
+ AttributeSet attrs) {
+ return new AppCompatMultiAutoCompleteTextView(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatRatingBar createRatingBar(Context context, AttributeSet attrs) {
+ return new AppCompatRatingBar(context, attrs);
+ }
+
+ @NonNull
+ protected AppCompatSeekBar createSeekBar(Context context, AttributeSet attrs) {
+ return new AppCompatSeekBar(context, attrs);
+ }
+
+ private void verifyNotNull(View view, String name) {
+ if (view == null) {
+ throw new IllegalStateException(this.getClass().getName()
+ + " asked to inflate view for <" + name + ">, but returned null");
+ }
+ }
+
+ @Nullable
+ protected View createView(Context context, String name, AttributeSet attrs) {
+ return null;
+ }
+
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
@@ -165,14 +261,14 @@
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
- final View view = createView(context, name, sClassPrefixList[i]);
+ final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
- return createView(context, name, null);
+ return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
@@ -209,7 +305,7 @@
a.recycle();
}
- private View createView(Context context, String name, String prefix)
+ private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
diff --git a/v7/appcompat/src/main/java/android/support/v7/view/ContextThemeWrapper.java b/v7/appcompat/src/main/java/android/support/v7/view/ContextThemeWrapper.java
index aa5b36e..cc63480 100644
--- a/v7/appcompat/src/main/java/android/support/v7/view/ContextThemeWrapper.java
+++ b/v7/appcompat/src/main/java/android/support/v7/view/ContextThemeWrapper.java
@@ -16,26 +16,19 @@
package android.support.v7.view;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
-import android.support.annotation.RestrictTo;
import android.support.annotation.StyleRes;
import android.support.v7.appcompat.R;
import android.view.LayoutInflater;
/**
- * A ContextWrapper that allows you to modify the theme from what is in the
- * wrapped context.
- *
- * @hide
+ * A context wrapper that allows you to modify or replace the theme of the wrapped context.
*/
-@RestrictTo(LIBRARY_GROUP)
public class ContextThemeWrapper extends ContextWrapper {
private int mThemeResource;
private Resources.Theme mTheme;
@@ -110,15 +103,6 @@
mOverrideConfiguration = new Configuration(overrideConfiguration);
}
- /**
- * Used by ActivityThread to apply the overridden configuration to onConfigurationChange
- * callbacks.
- * @hide
- */
- public Configuration getOverrideConfiguration() {
- return mOverrideConfiguration;
- }
-
@Override
public Resources getResources() {
return getResourcesInternal();
@@ -144,6 +128,10 @@
}
}
+ /**
+ * Returns the resource ID of the theme that is to be applied on top of the base context's
+ * theme.
+ */
public int getThemeResId() {
return mThemeResource;
}
diff --git a/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java b/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
index 73499cf..564bbfc 100644
--- a/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
+++ b/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
@@ -404,14 +404,14 @@
final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT;
mLastPosition = nextMenuPosition;
- final int parentOffsetLeft;
- final int parentOffsetTop;
+ final int parentOffsetX;
+ final int parentOffsetY;
if (Build.VERSION.SDK_INT >= 26) {
// Anchor the submenu directly to the parent menu item view. This allows for
// accurate submenu positioning when the parent menu is being moved.
popupWindow.setAnchorView(parentView);
- parentOffsetLeft = 0;
- parentOffsetTop = 0;
+ parentOffsetX = 0;
+ parentOffsetY = 0;
} else {
// Framework does not allow anchoring to a view in another popup window. Use the
// same top-level anchor as the parent menu is using, with appropriate offsets.
@@ -428,10 +428,19 @@
final int[] parentViewScreenLocation = new int[2];
parentView.getLocationOnScreen(parentViewScreenLocation);
+ // For Gravity.LEFT case, the baseline is just the left border of the view. So we
+ // can use the X of the location directly. But for Gravity.RIGHT case, the baseline
+ // is the right border. So we need add view's width with the location to make the
+ // baseline as the right border correctly.
+ if ((mDropDownGravity & (Gravity.RIGHT | Gravity.LEFT)) == Gravity.RIGHT) {
+ anchorScreenLocation[0] += mAnchorView.getWidth();
+ parentViewScreenLocation[0] += parentView.getWidth();
+ }
+
// If used as horizontal/vertical offsets, these values would position the submenu
// at the exact same position as the parent item.
- parentOffsetLeft = parentViewScreenLocation[0] - anchorScreenLocation[0];
- parentOffsetTop = parentViewScreenLocation[1] - anchorScreenLocation[1];
+ parentOffsetX = parentViewScreenLocation[0] - anchorScreenLocation[0];
+ parentOffsetY = parentViewScreenLocation[1] - anchorScreenLocation[1];
}
// Adjust the horizontal offset to display the submenu to the right or to the left
@@ -441,22 +450,22 @@
final int x;
if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) {
if (showOnRight) {
- x = parentOffsetLeft + menuWidth;
+ x = parentOffsetX + menuWidth;
} else {
- x = parentOffsetLeft - parentView.getWidth();
+ x = parentOffsetX - parentView.getWidth();
}
} else {
if (showOnRight) {
- x = parentOffsetLeft + parentView.getWidth();
+ x = parentOffsetX + parentView.getWidth();
} else {
- x = parentOffsetLeft - menuWidth;
+ x = parentOffsetX - menuWidth;
}
}
popupWindow.setHorizontalOffset(x);
// Vertically align with the parent item.
popupWindow.setOverlapAnchor(true);
- popupWindow.setVerticalOffset(parentOffsetTop);
+ popupWindow.setVerticalOffset(parentOffsetY);
} else {
if (mHasXOffset) {
popupWindow.setHorizontalOffset(mXOffset);
diff --git a/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java b/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
index f707c8f..dc20aa1 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
@@ -56,7 +56,7 @@
TooltipPopup(Context context) {
mContext = context;
- mContentView = LayoutInflater.from(mContext).inflate(R.layout.tooltip, null);
+ mContentView = LayoutInflater.from(mContext).inflate(R.layout.abc_tooltip, null);
mMessageView = (TextView) mContentView.findViewById(R.id.message);
mLayoutParams.setTitle(getClass().getSimpleName());
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index f986869..a85e4ab 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -121,6 +121,21 @@
<activity
android:name="android.support.v7.app.AppCompatVectorDrawableIntegrationActivity"/>
+ <activity
+ android:name="android.support.v7.app.AppCompatInflaterDefaultActivity"/>
+
+ <activity
+ android:name="android.support.v7.app.AppCompatInflaterBadClassNameActivity"
+ android:theme="@style/Theme.CustomInflaterBadClassName"/>
+
+ <activity
+ android:name="android.support.v7.app.AppCompatInflaterNullActivity"
+ android:theme="@style/Theme.CustomInflaterNull"/>
+
+ <activity
+ android:name="android.support.v7.app.AppCompatInflaterCustomActivity"
+ android:theme="@style/Theme.CustomInflater"/>
+
</application>
</manifest>
diff --git a/v7/appcompat/tests/res/drawable/black_rect.xml b/v7/appcompat/tests/res/drawable/black_rect.xml
new file mode 100644
index 0000000..d1cd0c2
--- /dev/null
+++ b/v7/appcompat/tests/res/drawable/black_rect.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml b/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml
new file mode 100644
index 0000000..3a27041
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1" />
+
+ <android.support.v7.widget.AppCompatButton
+ android:id="@+id/ac_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1" />
+
+ <TextView
+ android:id="@+id/textview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2" />
+
+ <android.support.v7.widget.AppCompatTextView
+ android:id="@+id/ac_textview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2" />
+
+ <RadioButton
+ android:id="@+id/radiobutton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1" />
+
+ <ImageButton
+ android:id="@+id/imagebutton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/test_drawable_blue" />
+
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/planets_array" />
+
+ <ToggleButton
+ android:id="@+id/togglebutton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text1" />
+
+ <ScrollView
+ android:id="@+id/scrollview"
+ android:layout_width="match_parent"
+ android:layout_height="100dp" />
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml b/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
index 3841206..1ca3459 100644
--- a/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
+++ b/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
@@ -77,6 +77,13 @@
android:background="@drawable/test_background_green" />
<android.support.v7.widget.AppCompatTextView
+ android:id="@+id/view_untinted_deferred"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/sample_text2"
+ android:background="@drawable/black_rect" />
+
+ <android.support.v7.widget.AppCompatTextView
android:id="@+id/view_text_color_hex"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/v7/appcompat/tests/res/values/styles.xml b/v7/appcompat/tests/res/values/styles.xml
index 68aa09f..9693b3a 100644
--- a/v7/appcompat/tests/res/values/styles.xml
+++ b/v7/appcompat/tests/res/values/styles.xml
@@ -31,6 +31,18 @@
<item name="android:textColorLink">@color/color_state_list_link</item>
</style>
+ <style name="Theme.CustomInflater" parent="@style/Theme.AppCompat.Light">
+ <item name="viewInflaterClass">android.support.v7.app.inflater.CustomViewInflater</item>
+ </style>
+
+ <style name="Theme.CustomInflaterBadClassName" parent="@style/Theme.AppCompat.Light">
+ <item name="viewInflaterClass">invalid.class.name</item>
+ </style>
+
+ <style name="Theme.CustomInflaterNull" parent="@style/Theme.AppCompat.Light">
+ <item name="viewInflaterClass">@null</item>
+ </style>
+
<style name="TextStyleAllCapsOn" parent="@android:style/TextAppearance.Medium">
<item name="textAllCaps">true</item>
</style>
@@ -86,4 +98,6 @@
<style name="TextView_Typeface_Monospace">
<item name="android:typeface">monospace</item>
</style>
+
+ <style name="TextAppearance" parent="TextAppearance.AppCompat" />
</resources>
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java
new file mode 100644
index 0000000..2d64277
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterBadClassNameActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_inflater_activity;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java
new file mode 100644
index 0000000..5f6e824
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterBadClassNameTest extends
+ AppCompatInflaterPassTest<AppCompatInflaterBadClassNameActivity> {
+ public AppCompatInflaterBadClassNameTest() {
+ super(AppCompatInflaterBadClassNameActivity.class);
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java
new file mode 100644
index 0000000..7ec9cdc
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterCustomActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_inflater_activity;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java
new file mode 100644
index 0000000..1e66635
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v7.app.inflater.CustomViewInflater;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.support.v7.widget.AppCompatSpinner;
+import android.support.v7.widget.AppCompatTextView;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Testing the custom view inflation where app-specific views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterCustomTest {
+ private ViewGroup mContainer;
+ private AppCompatInflaterCustomActivity mActivity;
+
+ @Rule
+ public final ActivityTestRule<AppCompatInflaterCustomActivity> mActivityTestRule =
+ new ActivityTestRule<>(AppCompatInflaterCustomActivity.class);
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityTestRule.getActivity();
+ mContainer = mActivity.findViewById(R.id.container);
+ }
+
+ @Test
+ public void testViewClasses() {
+ // View defined as <Button> should be inflated as CustomButton
+ assertEquals(CustomViewInflater.CustomButton.class,
+ mContainer.findViewById(R.id.button).getClass());
+
+ // View defined as <AppCompatButton> should be inflated as AppCompatButton
+ assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.ac_button).getClass());
+
+ // View defined as <TextView> should be inflated as CustomTextView
+ assertEquals(CustomViewInflater.CustomTextView.class,
+ mContainer.findViewById(R.id.textview).getClass());
+
+ // View defined as <AppCompatTextView> should be inflated as AppCompatTextView
+ assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.ac_textview).getClass());
+
+ // View defined as <RadioButton> should be inflated as AppCompatRadioButton
+ assertEquals(AppCompatRadioButton.class,
+ mContainer.findViewById(R.id.radiobutton).getClass());
+
+ // View defined as <ImageButton> should be inflated as CustomImageButton
+ assertEquals(CustomViewInflater.CustomImageButton.class,
+ mContainer.findViewById(R.id.imagebutton).getClass());
+
+ // View defined as <Spinner> should be inflated as AppCompatSpinner
+ assertEquals(AppCompatSpinner.class, mContainer.findViewById(R.id.spinner).getClass());
+
+ // View defined as <ToggleButton> should be inflated as CustomToggleButton
+ assertEquals(CustomViewInflater.CustomToggleButton.class,
+ mContainer.findViewById(R.id.togglebutton).getClass());
+
+ // View defined as <ScrollView> should be inflated as ScrollView
+ assertEquals(ScrollView.class,
+ mContainer.findViewById(R.id.scrollview).getClass());
+ }
+
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java
new file mode 100644
index 0000000..28b99ce
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterDefaultActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_inflater_activity;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java
new file mode 100644
index 0000000..d96060b
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterDefaultTest extends
+ AppCompatInflaterPassTest<AppCompatInflaterDefaultActivity> {
+ public AppCompatInflaterDefaultTest() {
+ super(AppCompatInflaterDefaultActivity.class);
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java
new file mode 100644
index 0000000..db75790
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterNullActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_inflater_activity;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java
new file mode 100644
index 0000000..b1d39e1
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterNullTest extends
+ AppCompatInflaterPassTest<AppCompatInflaterNullActivity> {
+ public AppCompatInflaterNullTest() {
+ super(AppCompatInflaterNullActivity.class);
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java
new file mode 100644
index 0000000..e8a2763
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.app;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatImageButton;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.support.v7.widget.AppCompatSpinner;
+import android.support.v7.widget.AppCompatTextView;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.ToggleButton;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public abstract class AppCompatInflaterPassTest<A extends BaseTestActivity> {
+ private ViewGroup mContainer;
+ private A mActivity;
+
+ @Rule
+ public final ActivityTestRule<A> mActivityTestRule;
+
+ public AppCompatInflaterPassTest(Class clazz) {
+ mActivityTestRule = new ActivityTestRule<A>(clazz);
+ }
+
+ @Before
+ public void setUp() {
+ mActivity = mActivityTestRule.getActivity();
+ mContainer = mActivity.findViewById(R.id.container);
+ }
+
+ @Test
+ public void testViewClasses() {
+ // View defined as <Button> should be inflated as AppCompatButton
+ assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.button).getClass());
+
+ // View defined as <AppCompatButton> should be inflated as AppCompatButton
+ assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.ac_button).getClass());
+
+ // View defined as <TextView> should be inflated as AppCompatTextView
+ assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.textview).getClass());
+
+ // View defined as <AppCompatTextView> should be inflated as AppCompatTextView
+ assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.ac_textview).getClass());
+
+ // View defined as <RadioButton> should be inflated as AppCompatRadioButton
+ assertEquals(AppCompatRadioButton.class,
+ mContainer.findViewById(R.id.radiobutton).getClass());
+
+ // View defined as <ImageButton> should be inflated as AppCompatImageButton
+ assertEquals(AppCompatImageButton.class,
+ mContainer.findViewById(R.id.imagebutton).getClass());
+
+ // View defined as <Spinner> should be inflated as AppCompatSpinner
+ assertEquals(AppCompatSpinner.class, mContainer.findViewById(R.id.spinner).getClass());
+
+ // View defined as <ToggleButton> should be inflated as ToggleButton
+ assertEquals(ToggleButton.class,
+ mContainer.findViewById(R.id.togglebutton).getClass());
+
+ // View defined as <ScrollView> should be inflated as ScrollView
+ assertEquals(ScrollView.class,
+ mContainer.findViewById(R.id.scrollview).getClass());
+ }
+
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
index 2981ad4..d42174f 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
@@ -23,13 +23,15 @@
import static android.support.v7.app.NightModeActivity.TOP_ACTIVITY;
import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.AppCompatActivityUtils;
+import android.support.testutils.RecreatedAppCompatActivity;
import android.support.v4.content.ContextCompat;
import android.support.v7.appcompat.test.R;
@@ -38,6 +40,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@LargeTest
@RunWith(AndroidJUnit4.class)
public class NightModeTestCase {
@@ -100,30 +105,31 @@
TwilightManager.setInstance(twilightManager);
final NightModeActivity activity = mActivityTestRule.getActivity();
- final AppCompatDelegateImplV14 delegate = (AppCompatDelegateImplV14) activity.getDelegate();
// Verify that we're currently in day mode
onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
- // Now set MODE_NIGHT_AUTO so that we will change to night mode automatically
- setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+ // Set MODE_NIGHT_AUTO so that we will change to night mode automatically
+ final NightModeActivity newActivity =
+ setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+ final AppCompatDelegateImplV14 newDelegate =
+ (AppCompatDelegateImplV14) newActivity.getDelegate();
- // Assert that the original Activity has not been destroyed yet
- assertFalse(activity.isDestroyed());
-
- // Now update the fake twilight manager to be in night and trigger a fake 'time' change
+ // Update the fake twilight manager to be in night and trigger a fake 'time' change
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
twilightManager.setIsNight(true);
- delegate.getAutoNightModeManager().dispatchTimeChanged();
+ newDelegate.getAutoNightModeManager().dispatchTimeChanged();
}
});
- // Now wait for the recreate
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
+ assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
+ // At this point recreate that has been triggered by dispatchTimeChanged call
+ // has completed
- // Now check that the text has changed, signifying that night resources are being used
+ // Check that the text has changed, signifying that night resources are being used
onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_NIGHT)));
}
@@ -133,28 +139,30 @@
final FakeTwilightManager twilightManager = new FakeTwilightManager();
TwilightManager.setInstance(twilightManager);
- final NightModeActivity activity = mActivityTestRule.getActivity();
+ NightModeActivity activity = mActivityTestRule.getActivity();
// Set MODE_NIGHT_AUTO so that we will change to night mode automatically
- setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+ activity = setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
// Verify that we're currently in day mode
onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
+ final NightModeActivity toTest = activity;
+
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
final Instrumentation instrumentation =
InstrumentationRegistry.getInstrumentation();
// Now fool the Activity into thinking that it has gone into the background
- instrumentation.callActivityOnPause(activity);
- instrumentation.callActivityOnStop(activity);
+ instrumentation.callActivityOnPause(toTest);
+ instrumentation.callActivityOnStop(toTest);
// Now update the twilight manager while the Activity is in the 'background'
twilightManager.setIsNight(true);
// Now tell the Activity that it has gone into the foreground again
- instrumentation.callActivityOnStart(activity);
- instrumentation.callActivityOnResume(activity);
+ instrumentation.callActivityOnStart(toTest);
+ instrumentation.callActivityOnResume(toTest);
}
});
@@ -179,7 +187,8 @@
}
}
- private void setLocalNightModeAndWaitForRecreate(final AppCompatActivity activity,
+ private NightModeActivity setLocalNightModeAndWaitForRecreate(
+ final NightModeActivity activity,
@AppCompatDelegate.NightMode final int nightMode) throws Throwable {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
mActivityTestRule.runOnUiThread(new Runnable() {
@@ -188,6 +197,11 @@
activity.getDelegate().setLocalNightMode(nightMode);
}
});
+ final NightModeActivity result =
+ AppCompatActivityUtils.recreateActivity(mActivityTestRule, activity);
+ AppCompatActivityUtils.waitForExecution(mActivityTestRule);
+
instrumentation.waitForIdleSync();
+ return result;
}
}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/inflater/CustomViewInflater.java b/v7/appcompat/tests/src/android/support/v7/app/inflater/CustomViewInflater.java
new file mode 100644
index 0000000..7876499
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/inflater/CustomViewInflater.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app.inflater;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatViewInflater;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatImageButton;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ToggleButton;
+
+/**
+ * Custom view inflater that takes over the inflation of a few widget types.
+ */
+public class CustomViewInflater extends AppCompatViewInflater {
+ public static class CustomTextView extends AppCompatTextView {
+ public CustomTextView(Context context) {
+ super(context);
+ }
+
+ public CustomTextView(Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomTextView(Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ }
+
+ public static class CustomButton extends AppCompatButton {
+ public CustomButton(Context context) {
+ super(context);
+ }
+
+ public CustomButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ }
+
+ public static class CustomImageButton extends AppCompatImageButton {
+ public CustomImageButton(Context context) {
+ super(context);
+ }
+
+ public CustomImageButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+ }
+
+ public static class CustomToggleButton extends ToggleButton {
+ public CustomToggleButton(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public CustomToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CustomToggleButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CustomToggleButton(Context context) {
+ super(context);
+ }
+ }
+
+ @NonNull
+ @Override
+ protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+ return new CustomButton(context, attrs);
+ }
+
+ @NonNull
+ @Override
+ protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
+ return new CustomTextView(context, attrs);
+ }
+
+ @NonNull
+ @Override
+ protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
+ return new CustomImageButton(context, attrs);
+ }
+
+ @Nullable
+ @Override
+ protected View createView(Context context, String name, AttributeSet attrs) {
+ if (name.equals("ToggleButton")) {
+ return new CustomToggleButton(context, attrs);
+ }
+ return null;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java b/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java
new file mode 100644
index 0000000..21c4ffc
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app.inflater;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatViewInflater;
+import android.support.v7.widget.AppCompatButton;
+import android.util.AttributeSet;
+
+/**
+ * Custom view inflater that declares that it takes over the view inflation but
+ * does not honor the contract to return non-null instance in its
+ * {@link #createButton(Context, AttributeSet)} method.
+ */
+public class MisbehavingViewInflater extends AppCompatViewInflater {
+ @NonNull
+ @Override
+ protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+ return null;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java b/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
index 8ed22ad..e4dbf26 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
@@ -19,7 +19,7 @@
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
+import android.support.testutils.RecreatedAppCompatActivity;
import android.support.v7.app.AppCompatCallback;
import android.support.v7.appcompat.test.R;
import android.support.v7.view.ActionMode;
@@ -28,7 +28,7 @@
import android.view.MenuItem;
import android.view.WindowManager;
-public abstract class BaseTestActivity extends AppCompatActivity {
+public abstract class BaseTestActivity extends RecreatedAppCompatActivity {
private Menu mMenu;
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
index 574ed6b..6e4516e 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
@@ -221,7 +221,7 @@
+ ": expected all drawable colors to be "
+ formatColorToHex(color)
+ " but at position (" + centerX + "," + centerY + ") out of ("
- + bitmap.getWidth() + "," + bitmap.getHeight() + ") found"
+ + bitmap.getWidth() + "," + bitmap.getHeight() + ") found "
+ formatColorToHex(colorAtCenterPixel);
if (throwExceptionIfFails) {
throw new RuntimeException(mismatchDescription);
diff --git a/v7/appcompat/tests/src/android/support/v7/view/ContextThemeWrapperTest.java b/v7/appcompat/tests/src/android/support/v7/view/ContextThemeWrapperTest.java
new file mode 100644
index 0000000..c9a6058
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/view/ContextThemeWrapperTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.view;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.appcompat.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ContextThemeWrapperTest {
+ private static final int SYSTEM_DEFAULT_THEME = 0;
+
+ private Context mContext;
+
+ private static class MockContextThemeWrapper extends ContextThemeWrapper {
+ boolean mIsOnApplyThemeResourceCalled;
+ MockContextThemeWrapper(Context base, int themeres) {
+ super(base, themeres);
+ }
+
+ @Override
+ protected void onApplyThemeResource(Theme theme, int resid, boolean first) {
+ mIsOnApplyThemeResourceCalled = true;
+ super.onApplyThemeResource(theme, resid, first);
+ }
+ }
+
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void testConstructor() {
+ new ContextThemeWrapper();
+
+ new ContextThemeWrapper(mContext, R.style.TextAppearance);
+
+ new ContextThemeWrapper(mContext, mContext.getTheme());
+ }
+
+ @Test
+ public void testAccessTheme() {
+ ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(
+ mContext, SYSTEM_DEFAULT_THEME);
+ // set Theme to TextAppearance
+ contextThemeWrapper.setTheme(R.style.TextAppearance);
+ TypedArray ta = contextThemeWrapper.getTheme().obtainStyledAttributes(
+ R.styleable.TextAppearance);
+
+ // assert theme style of TextAppearance
+ verifyIdenticalTextAppearanceStyle(ta);
+ }
+
+ @Test
+ public void testGetSystemService() {
+ // Note that we can't use Mockito since ContextThemeWrapper.onApplyThemeResource is
+ // protected
+ final MockContextThemeWrapper contextThemeWrapper =
+ new MockContextThemeWrapper(mContext, R.style.TextAppearance);
+ contextThemeWrapper.getTheme();
+ assertTrue(contextThemeWrapper.mIsOnApplyThemeResourceCalled);
+
+ // All service get from contextThemeWrapper just the same as this context get,
+ // except Context.LAYOUT_INFLATER_SERVICE.
+ assertEquals(mContext.getSystemService(Context.ACTIVITY_SERVICE),
+ contextThemeWrapper.getSystemService(Context.ACTIVITY_SERVICE));
+ assertNotSame(mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE),
+ contextThemeWrapper.getSystemService(Context.LAYOUT_INFLATER_SERVICE));
+ }
+
+ @Test
+ public void testAttachBaseContext() {
+ assertTrue((new ContextThemeWrapper() {
+ public boolean test() {
+ // Set two different context to ContextThemeWrapper
+ // it should throw a exception when set it at second time.
+ // As ContextThemeWrapper is a context, we will attachBaseContext to
+ // two different ContextThemeWrapper instances.
+ try {
+ attachBaseContext(new ContextThemeWrapper(mContext,
+ R.style.TextAppearance));
+ } catch (IllegalStateException e) {
+ fail("test attachBaseContext fail");
+ }
+
+ try {
+ attachBaseContext(new ContextThemeWrapper());
+ fail("test attachBaseContext fail");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ return true;
+ }
+ }).test());
+ }
+
+ @Test
+ public void testApplyOverrideConfiguration() {
+ final int realDensity = mContext.getResources().getConfiguration().densityDpi;
+ final int expectedDensity = realDensity + 1;
+
+ ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(
+ mContext, SYSTEM_DEFAULT_THEME);
+
+ Configuration overrideConfig = new Configuration();
+ overrideConfig.densityDpi = expectedDensity;
+ contextThemeWrapper.applyOverrideConfiguration(overrideConfig);
+
+ Configuration actualConfiguration = contextThemeWrapper.getResources().getConfiguration();
+ assertEquals(expectedDensity, actualConfiguration.densityDpi);
+ }
+
+ private void verifyIdenticalTextAppearanceStyle(TypedArray ta) {
+ final int defValue = -1;
+ // get Theme and assert
+ Resources.Theme expected = mContext.getResources().newTheme();
+ expected.setTo(mContext.getTheme());
+ expected.applyStyle(R.style.TextAppearance, true);
+ TypedArray expectedTa = expected.obtainStyledAttributes(R.styleable.TextAppearance);
+ assertEquals(expectedTa.getIndexCount(), ta.getIndexCount());
+ assertEquals(expectedTa.getColor(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor,
+ defValue),
+ ta.getColor(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor,
+ defValue));
+ assertEquals(expectedTa.getColor(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textColorHint,
+ defValue),
+ ta.getColor(
+ android.support.v7.appcompat.R.styleable
+ .TextAppearance_android_textColorHint,
+ defValue));
+ assertEquals(expectedTa.getColor(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textColorLink,
+ defValue),
+ ta.getColor(
+ android.support.v7.appcompat.R.styleable
+ .TextAppearance_android_textColorLink,
+ defValue));
+ assertEquals(expectedTa.getDimension(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
+ defValue),
+ ta.getDimension(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
+ defValue), 0.0f);
+ assertEquals(expectedTa.getInt(
+ android.support.v7.appcompat.R.styleable.TextAppearance_android_textStyle,
+ defValue),
+ ta.getInt(android.support.v7.appcompat.R.styleable
+ .TextAppearance_android_textStyle,
+ defValue));
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
index 34890ed..eb52653 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
@@ -16,29 +16,40 @@
package android.support.v7.widget;
import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.v7.testutils.TestUtilsActions.setEnabled;
import static android.support.v7.testutils.TestUtilsActions.setTextAppearance;
+import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
+import android.support.annotation.ColorInt;
import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.test.R;
+import android.view.View;
import android.widget.TextView;
import org.junit.Test;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
/**
* In addition to all tinting-related tests done by the base class, this class provides
* tests specific to {@link AppCompatTextView} class.
@@ -51,6 +62,43 @@
super(AppCompatTextViewActivity.class);
}
+ /**
+ * This method tests that background tinting is applied when the call to
+ * {@link android.support.v4.view.ViewCompat#setBackgroundTintList(View, ColorStateList)}
+ * is done as a deferred event.
+ */
+ @Test
+ @MediumTest
+ public void testDeferredBackgroundTinting() throws Throwable {
+ onView(withId(R.id.view_untinted_deferred))
+ .check(matches(isBackground(0xff000000, true)));
+
+ final @ColorInt int oceanDefault = ResourcesCompat.getColor(
+ mResources, R.color.ocean_default, null);
+
+ final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
+ mResources, R.color.color_state_list_ocean, null);
+
+ // Emulate delay in kicking off the call to ViewCompat.setBackgroundTintList
+ Thread.sleep(200);
+ final CountDownLatch latch = new CountDownLatch(1);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TextView view = mActivity.findViewById(R.id.view_untinted_deferred);
+ ViewCompat.setBackgroundTintList(view, oceanColor);
+ latch.countDown();
+ }
+ });
+
+ assertTrue(latch.await(2, TimeUnit.SECONDS));
+
+ // Check that the background has switched to the matching entry in the newly set
+ // color state list.
+ onView(withId(R.id.view_untinted_deferred))
+ .check(matches(isBackground(oceanDefault, true)));
+ }
+
@Test
public void testAllCaps() {
final String text1 = mResources.getString(R.string.sample_text1);
@@ -306,10 +354,11 @@
assertEquals(Typeface.SERIF, textView.getTypeface());
}
+ @SdkSuppress(minSdkVersion = 16)
@Test
@UiThreadTest
public void testfontFamilyNamespaceHierarchy() {
- // This view has fontFamilyset in both the app and android namespace. App should be used.
+ // This view has fontFamily set in both the app and android namespace. App should be used.
TextView textView = mContainer.findViewById(R.id.textview_app_and_android_fontfamily);
assertEquals(Typeface.MONOSPACE, textView.getTypeface());
diff --git a/v7/mediarouter/OWNERS b/v7/mediarouter/OWNERS
new file mode 100644
index 0000000..e67af3b
--- /dev/null
+++ b/v7/mediarouter/OWNERS
@@ -0,0 +1,3 @@
+akersten@google.com
+jaewan@google.com
+sungsoo@google.com
diff --git a/v7/mediarouter/src/android/support/v7/media/package.html b/v7/mediarouter/src/android/support/v7/media/package.html
index 0866a42..be2aaf2 100644
--- a/v7/mediarouter/src/android/support/v7/media/package.html
+++ b/v7/mediarouter/src/android/support/v7/media/package.html
@@ -4,7 +4,6 @@
<p>Contains APIs that control the routing of media channels and streams from the current device
to external speakers and destination devices.</p>
-<p>Compatible with API level 7 and higher.</p>
</body>
</html>
diff --git a/v7/preference/api/current.txt b/v7/preference/api/current.txt
index 04c7329..1b2a746 100644
--- a/v7/preference/api/current.txt
+++ b/v7/preference/api/current.txt
@@ -275,6 +275,7 @@
method protected void dispatchRestoreInstanceState(android.os.Bundle);
method protected void dispatchSaveInstanceState(android.os.Bundle);
method public android.support.v7.preference.Preference findPreference(java.lang.CharSequence);
+ method public int getInitialExpandedChildrenCount();
method public android.support.v7.preference.Preference getPreference(int);
method public int getPreferenceCount();
method protected boolean isOnSameScreenAsChildren();
@@ -282,6 +283,7 @@
method protected boolean onPrepareAddPreference(android.support.v7.preference.Preference);
method public void removeAll();
method public boolean removePreference(android.support.v7.preference.Preference);
+ method public void setInitialExpandedChildrenCount(int);
method public void setOrderingAsAdded(boolean);
}
diff --git a/v7/preference/res/drawable/ic_arrow_down_24dp.xml b/v7/preference/res/drawable/ic_arrow_down_24dp.xml
new file mode 100644
index 0000000..7c5866d
--- /dev/null
+++ b/v7/preference/res/drawable/ic_arrow_down_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorAccent">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
+</vector>
diff --git a/v7/preference/res/layout-v7/expand_button.xml b/v7/preference/res/layout-v7/expand_button.xml
new file mode 100644
index 0000000..35faae8
--- /dev/null
+++ b/v7/preference/res/layout-v7/expand_button.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<!-- Based off frameworks/base/core/res/res/layout/preference_material.xml -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@android:id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal"
+ android:layout_marginStart="-4dp"
+ android:minWidth="60dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+ <android.support.v7.internal.widget.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxWidth="48dp"
+ android:maxHeight="48dp"/>
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"
+ android:ellipsize="marquee"
+ android:singleLine="true"/>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/v7/preference/res/values/attrs.xml b/v7/preference/res/values/attrs.xml
index f204d45..8ab8de1 100644
--- a/v7/preference/res/values/attrs.xml
+++ b/v7/preference/res/values/attrs.xml
@@ -91,6 +91,15 @@
default to alphabetic for those without the order attribute. -->
<attr name="orderingFromXml" format="boolean" />
<attr name="android:orderingFromXml" />
+ <!-- The maximal number of children that are shown when the preference group is launched
+ where the rest of the children will be hidden. If some children are hidden an expand
+ button will be provided to show all the hidden children.
+ Any child in any level of the hierarchy that is also a preference group (e.g.
+ preference category) will not be counted towards the limit. But instead the children of
+ such group will be counted.
+ By default, all children will be shown, so the default value of this attribute is equal
+ to Integer.MAX_VALUE. -->
+ <attr name="initialExpandedChildrenCount" format="integer" />
</declare-styleable>
<!-- Base attributes available to Preference. -->
diff --git a/v7/preference/res/values/strings.xml b/v7/preference/res/values/strings.xml
index 3414e44..1788f13 100644
--- a/v7/preference/res/values/strings.xml
+++ b/v7/preference/res/values/strings.xml
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="v7_preference_on">ON</string>
<string name="v7_preference_off">OFF</string>
+ <!-- Title for the preference expand button [CHAR LIMIT=30] -->
+ <string name="expand_button_title">Advanced</string>
+ <!-- Summary for the preference expand button. This is used to format preference summaries as a list. [CHAR_LIMIT=NONE] -->
+ <string name="summary_collapsed_preference_list"><xliff:g id="current_items">%1$s</xliff:g>, <xliff:g id="added_items">%2$s</xliff:g></string>
</resources>
diff --git a/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
new file mode 100644
index 0000000..e15ca18
--- /dev/null
+++ b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.preference;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to handle advanced children display logic with collapsible functionality.
+ */
+final class CollapsiblePreferenceGroupController
+ implements PreferenceGroup.PreferenceInstanceStateCallback {
+
+ private final PreferenceGroupAdapter mPreferenceGroupAdapter;
+ private int mMaxPreferenceToShow;
+ private final Context mContext;
+
+ CollapsiblePreferenceGroupController(PreferenceGroup preferenceGroup,
+ PreferenceGroupAdapter preferenceGroupAdapter) {
+ mPreferenceGroupAdapter = preferenceGroupAdapter;
+ mMaxPreferenceToShow = preferenceGroup.getInitialExpandedChildrenCount();
+ mContext = preferenceGroup.getContext();
+ preferenceGroup.setPreferenceInstanceStateCallback(this);
+ }
+
+ /**
+ * Creates the visible portion of the flattened preferences.
+ *
+ * @param flattenedPreferenceList the flattened children of the preference group
+ * @return the visible portion of the flattened preferences
+ */
+ public List<Preference> createVisiblePreferencesList(List<Preference> flattenedPreferenceList) {
+ int visiblePreferenceCount = 0;
+ final List<Preference> visiblePreferenceList =
+ new ArrayList<>(flattenedPreferenceList.size());
+ // Copy only the visible preferences to the active list up to the maximum specified
+ for (final Preference preference : flattenedPreferenceList) {
+ if (preference.isVisible()) {
+ if (visiblePreferenceCount < mMaxPreferenceToShow) {
+ visiblePreferenceList.add(preference);
+ }
+ // Do no count PreferenceGroup as expanded preference because the list of its child
+ // is already contained in the flattenedPreferenceList
+ if (!(preference instanceof PreferenceGroup)) {
+ visiblePreferenceCount++;
+ }
+ }
+ }
+ // If there are any visible preferences being hidden, add an expand button to show the rest
+ // of the preferences. Clicking the expand button will show all the visible preferences and
+ // reset mMaxPreferenceToShow
+ if (showLimitedChildren() && visiblePreferenceCount > mMaxPreferenceToShow) {
+ final ExpandButton expandButton = createExpandButton(visiblePreferenceList,
+ flattenedPreferenceList);
+ visiblePreferenceList.add(expandButton);
+ }
+ return visiblePreferenceList;
+ }
+
+ /**
+ * Called when a preference has changed its visibility.
+ *
+ * @param preference The preference whose visibility has changed.
+ * @return {@code true} if view update has been handled by this controller.
+ */
+ public boolean onPreferenceVisibilityChange(Preference preference) {
+ if (showLimitedChildren()) {
+ // We only want to show up to the max number of preferences. Preference visibility
+ // change can result in the expand button being added/removed, as well as expand button
+ // summary change. Rebulid the data to ensure the correct data is shown.
+ mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Parcelable saveInstanceState(Parcelable state) {
+ final SavedState myState = new SavedState(state);
+ myState.mMaxPreferenceToShow = mMaxPreferenceToShow;
+ return myState;
+ }
+
+ @Override
+ public Parcelable restoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in saveInstanceState
+ return state;
+ }
+ SavedState myState = (SavedState) state;
+ final int restoredMaxToShow = myState.mMaxPreferenceToShow;
+ if (mMaxPreferenceToShow != restoredMaxToShow) {
+ mMaxPreferenceToShow = restoredMaxToShow;
+ mPreferenceGroupAdapter.onPreferenceHierarchyChange(null);
+ }
+ return myState.getSuperState();
+ }
+
+ private ExpandButton createExpandButton(List<Preference> visiblePreferenceList,
+ List<Preference> flattenedPreferenceList) {
+ final ExpandButton preference = new ExpandButton(mContext, visiblePreferenceList,
+ flattenedPreferenceList);
+ preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ mMaxPreferenceToShow = Integer.MAX_VALUE;
+ mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference);
+ return true;
+ }
+ });
+ return preference;
+ }
+
+ private boolean showLimitedChildren() {
+ return mMaxPreferenceToShow != Integer.MAX_VALUE;
+ }
+
+ /**
+ * A {@link Preference} that provides capability to expand the collapsed items in the
+ * {@link PreferenceGroup}.
+ */
+ static class ExpandButton extends Preference {
+ ExpandButton(Context context, List<Preference> visiblePreferenceList,
+ List<Preference> flattenedPreferenceList) {
+ super(context);
+ initLayout();
+ setSummary(visiblePreferenceList, flattenedPreferenceList);
+ }
+
+ private void initLayout() {
+ setLayoutResource(R.layout.expand_button);
+ setIcon(R.drawable.ic_arrow_down_24dp);
+ setTitle(R.string.expand_button_title);
+ // Sets a high order so that the expand button will be placed at the bottom of the group
+ setOrder(999);
+ }
+
+ /*
+ * The summary of this will be the list of title for collapsed preferences. Iterate through
+ * the preferences not in the visible list and add its title to the summary text.
+ */
+ private void setSummary(List<Preference> visiblePreferenceList,
+ List<Preference> flattenedPreferenceList) {
+ final Preference lastVisiblePreference =
+ visiblePreferenceList.get(visiblePreferenceList.size() - 1);
+ final int collapsedIndex = flattenedPreferenceList.indexOf(lastVisiblePreference) + 1;
+ CharSequence summary = null;
+ for (int i = collapsedIndex; i < flattenedPreferenceList.size(); i++) {
+ final Preference preference = flattenedPreferenceList.get(i);
+ if (preference instanceof PreferenceGroup) {
+ continue;
+ }
+ final CharSequence title = preference.getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ if (summary == null) {
+ summary = title;
+ } else {
+ summary = getContext().getString(
+ R.string.summary_collapsed_preference_list, summary, title);
+ }
+ }
+ }
+ setSummary(summary);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ holder.setDividerAllowedAbove(false);
+ }
+ }
+
+ /**
+ * A class for managing the instance state of a {@link PreferenceGroup}.
+ */
+ static class SavedState extends Preference.BaseSavedState {
+ int mMaxPreferenceToShow;
+
+ SavedState(Parcel source) {
+ super(source);
+ mMaxPreferenceToShow = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mMaxPreferenceToShow);
+ }
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
index d285ee6..a951e70 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
@@ -22,7 +22,9 @@
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Parcelable;
import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
@@ -45,6 +47,7 @@
* </div>
*
* @attr name android:orderingFromXml
+ * @attr name initialExpandedChildrenCount
*/
public abstract class PreferenceGroup extends Preference {
/**
@@ -60,6 +63,9 @@
private boolean mAttachedToHierarchy = false;
+ private int mInitialExpandedChildrenCount = Integer.MAX_VALUE;
+ private PreferenceInstanceStateCallback mPreferenceInstanceStateCallback;
+
private final SimpleArrayMap<String, Long> mIdRecycleCache = new SimpleArrayMap<>();
private final Handler mHandler = new Handler();
private final Runnable mClearRecycleCacheRunnable = new Runnable() {
@@ -83,6 +89,11 @@
TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml,
R.styleable.PreferenceGroup_orderingFromXml, true);
+ if (a.hasValue(R.styleable.PreferenceGroup_initialExpandedChildrenCount)) {
+ mInitialExpandedChildrenCount = TypedArrayUtils.getInt(
+ a, R.styleable.PreferenceGroup_initialExpandedChildrenCount,
+ R.styleable.PreferenceGroup_initialExpandedChildrenCount, -1);
+ }
a.recycle();
}
@@ -120,6 +131,35 @@
}
/**
+ * Sets the maximal number of children that are shown when the preference group is launched
+ * where the rest of the children will be hidden.
+ * If some children are hidden an expand button will be provided to show all the hidden
+ * children. Any child in any level of the hierarchy that is also a preference group (e.g.
+ * preference category) will not be counted towards the limit. But instead the children of such
+ * group will be counted.
+ * By default, all children will be shown, so the default value of this attribute is equal to
+ * Integer.MAX_VALUE.
+ *
+ * @param expandedCount the number of children that is initially shown.
+ *
+ * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
+ */
+ public void setInitialExpandedChildrenCount(int expandedCount) {
+ mInitialExpandedChildrenCount = expandedCount;
+ }
+
+ /**
+ * Gets the maximal number of children that is initially shown.
+ *
+ * @return the maximal number of children that is initially shown.
+ *
+ * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
+ */
+ public int getInitialExpandedChildrenCount() {
+ return mInitialExpandedChildrenCount;
+ }
+
+ /**
* Called by the inflater to add an item to this group.
*/
public void addItemFromInflater(Preference preference) {
@@ -400,6 +440,44 @@
}
}
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (mPreferenceInstanceStateCallback != null) {
+ return mPreferenceInstanceStateCallback.saveInstanceState(superState);
+ }
+ return superState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (mPreferenceInstanceStateCallback != null) {
+ state = mPreferenceInstanceStateCallback.restoreInstanceState(state);
+ }
+ super.onRestoreInstanceState(state);
+ }
+
+ /**
+ * Sets the instance state callback.
+ *
+ * @param callback The callback.
+ * @see #onSaveInstanceState()
+ * @see #onRestoreInstanceState()
+ */
+ final void setPreferenceInstanceStateCallback(PreferenceInstanceStateCallback callback) {
+ mPreferenceInstanceStateCallback = callback;
+ }
+
+ /**
+ * Gets the instance state callback.
+ *
+ * @return the instance state callback.
+ */
+ @VisibleForTesting
+ final PreferenceInstanceStateCallback getPreferenceInstanceStateCallback() {
+ return mPreferenceInstanceStateCallback;
+ }
+
/**
* Interface for PreferenceGroup Adapters to implement so that
* {@link android.support.v14.preference.PreferenceFragment#scrollToPreference(String)} and
@@ -426,4 +504,29 @@
*/
int getPreferenceAdapterPosition(Preference preference);
}
+
+ /**
+ * Interface for callback to implement so that they can save and restore the preference group's
+ * instance state.
+ */
+ interface PreferenceInstanceStateCallback {
+
+ /**
+ * Save the internal state that can later be used to create a new instance with that
+ * same state.
+ *
+ * @param state The Parcelable to save the current dynamic state.
+ */
+ Parcelable saveInstanceState(Parcelable state);
+
+ /**
+ * Restore the previously saved state from the given parcelable.
+ *
+ * @param state The Parcelable that holds the previously saved state.
+ * @return the super state if data has been saved in the state in {@link saveInstanceState}
+ * or state otherwise
+ */
+ Parcelable restoreInstanceState(Parcelable state);
+ }
+
}
diff --git a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
index d1c630f..00a0c5b 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
@@ -22,6 +22,7 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.util.DiffUtil;
@@ -73,7 +74,9 @@
private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout();
- private Handler mHandler = new Handler();
+ private Handler mHandler;
+
+ private CollapsiblePreferenceGroupController mPreferenceGroupController;
private Runnable mSyncRunnable = new Runnable() {
@Override
@@ -117,7 +120,14 @@
}
public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
+ this(preferenceGroup, new Handler());
+ }
+
+ private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) {
mPreferenceGroup = preferenceGroup;
+ mHandler = handler;
+ mPreferenceGroupController =
+ new CollapsiblePreferenceGroupController(preferenceGroup, this);
// If this group gets or loses any children, let us know
mPreferenceGroup.setOnPreferenceChangeInternalListener(this);
@@ -134,6 +144,12 @@
syncMyPreferences();
}
+ @VisibleForTesting
+ static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup,
+ Handler handler) {
+ return new PreferenceGroupAdapter(preferenceGroup, handler);
+ }
+
private void syncMyPreferences() {
for (final Preference preference : mPreferenceListInternal) {
// Clear out the listeners in anticipation of some items being removed. This listener
@@ -143,13 +159,8 @@
final List<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size());
flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup);
- final List<Preference> visiblePreferenceList = new ArrayList<>(fullPreferenceList.size());
- // Copy only the visible preferences to the active list
- for (final Preference preference : fullPreferenceList) {
- if (preference.isVisible()) {
- visiblePreferenceList.add(preference);
- }
- }
+ final List<Preference> visiblePreferenceList =
+ mPreferenceGroupController.createVisiblePreferencesList(fullPreferenceList);
final List<Preference> oldVisibleList = mPreferenceList;
mPreferenceList = visiblePreferenceList;
@@ -277,6 +288,9 @@
if (!mPreferenceListInternal.contains(preference)) {
return;
}
+ if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) {
+ return;
+ }
if (preference.isVisible()) {
// The preference has become visible, we need to add it in the correct location.
diff --git a/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
new file mode 100644
index 0000000..4f53b9a
--- /dev/null
+++ b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.preference;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PreferenceGroupInitialExpandedChildrenCountTest {
+
+ private static final int INITIAL_EXPANDED_COUNT = 5;
+ private static final int TOTAL_PREFERENCE = 10;
+ private static final String PREFERENCE_TITLE_PREFIX = "Preference_";
+
+ private Context mContext;
+ private PreferenceManager mPreferenceManager;
+ private PreferenceScreen mScreen;
+ private Handler mHandler;
+ private List<Preference> mPreferenceList;
+
+ @Before
+ @UiThreadTest
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getTargetContext();
+ mPreferenceManager = new PreferenceManager(mContext);
+ mScreen = mPreferenceManager.createPreferenceScreen(mContext);
+
+ // Add 10 preferences to the screen and to the cache
+ mPreferenceList = new ArrayList<>();
+ createTestPreferences(mScreen, mPreferenceList, TOTAL_PREFERENCE);
+
+ // Execute the handler task immediately
+ mHandler = spy(new Handler());
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ Message message = (Message) args[0];
+ mHandler.dispatchMessage(message);
+ return null;
+ }
+ }).when(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+ }
+
+ /**
+ * Verifies that when PreferenceGroupAdapter is created, the PreferenceInstanceStateCallback
+ * is set on the PreferenceGroup.
+ */
+ @Test
+ @UiThreadTest
+ public void createPreferenceGroupAdapter_setPreferenceInstanceStateCallback() {
+ PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ assertNotNull(mScreen.getPreferenceInstanceStateCallback());
+ }
+
+ /**
+ * Verifies that PreferenceGroupAdapter is showing the preferences on the screen correctly with
+ * and without the collapsed child count set.
+ */
+ @Test
+ @UiThreadTest
+ public void createPreferenceGroupAdapter_displayTopLevelPreferences() {
+ // No limit, should display all 10 preferences
+ PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+ // Limit > child count, should display all 10 preferences
+ mScreen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE + 4);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+ // Limit = child count, should display all 10 preferences
+ mScreen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+ // Limit < child count, should display up to the limit + expand button
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ assertPreferencesAreCollapsed(preferenceGroupAdapter);
+ for (int i = 0; i < INITIAL_EXPANDED_COUNT; i++) {
+ assertEquals(mPreferenceList.get(i), preferenceGroupAdapter.getItem(i));
+ }
+ assertEquals(CollapsiblePreferenceGroupController.ExpandButton.class,
+ preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT).getClass());
+ }
+
+ /**
+ * Verifies that PreferenceGroupAdapter is showing nested preferences on the screen correctly
+ * with and without the collapsed child count set.
+ */
+ @Test
+ @UiThreadTest
+ public void createPreferenceGroupAdapter_displayNestedPreferences() {
+ final PreferenceScreen screen = mPreferenceManager.createPreferenceScreen(mContext);
+ final List<Preference> preferenceList = new ArrayList<>();
+
+ // Add 2 preferences and 2 categories to screen
+ createTestPreferences(screen, preferenceList, 2);
+ createTestPreferencesCategory(screen, preferenceList, 4);
+ createTestPreferencesCategory(screen, preferenceList, 4);
+
+ // No limit, should display all 10 preferences + 2 categories
+ PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+ assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+ // Limit > child count, should display all 10 preferences + 2 categories
+ screen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE + 4);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+ assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+ // Limit = child count, should display all 10 preferences + 2 categories
+ screen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+ assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+ // Limit < child count, should display 2 preferences and the first 3 preference in the
+ // category + expand button
+ screen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+ assertEquals(INITIAL_EXPANDED_COUNT + 2, preferenceGroupAdapter.getItemCount());
+ for (int i = 0; i <= INITIAL_EXPANDED_COUNT; i++) {
+ assertEquals(preferenceList.get(i), preferenceGroupAdapter.getItem(i));
+ }
+ assertEquals(CollapsiblePreferenceGroupController.ExpandButton.class,
+ preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT + 1).getClass());
+ }
+
+ /**
+ * Verifies that correct summary is set for the expand button.
+ */
+ @Test
+ @UiThreadTest
+ public void createPreferenceGroupAdapter_setExpandButtonSummary() {
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ // Preference 5 to Preference 9 are collapsed
+ CharSequence summary = mPreferenceList.get(INITIAL_EXPANDED_COUNT).getTitle();
+ for (int i = INITIAL_EXPANDED_COUNT + 1; i < TOTAL_PREFERENCE; i++) {
+ summary = mContext.getString(R.string.summary_collapsed_preference_list,
+ summary, mPreferenceList.get(i).getTitle());
+ }
+ final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+ assertEquals(summary, expandButton.getSummary());
+ }
+
+ /**
+ * Verifies that clicking the expand button will show all preferences.
+ */
+ @Test
+ @UiThreadTest
+ public void clickExpandButton_shouldShowAllPreferences() {
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+
+ // First showing 5 preference with expand button
+ PreferenceGroupAdapter preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ assertPreferencesAreCollapsed(preferenceGroupAdapter);
+
+ // Click the expand button, should review all preferences
+ final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+ expandButton.performClick();
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+ }
+
+ /**
+ * Verifies that when preference visibility changes, it will sync the preferences only if some
+ * preferences are collapsed.
+ */
+ @Test
+ @UiThreadTest
+ public void onPreferenceVisibilityChange_shouldSyncPreferencesIfCollapsed() {
+ // No limit set, should not sync preference
+ PreferenceGroupAdapter preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+ // Has limit set, should sync preference
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+ verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+
+ // Preferences expanded already, should not sync preference
+ final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+ expandButton.performClick();
+ reset(mHandler);
+ preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+ }
+
+ /**
+ * Verifies that the correct maximum number of preferences to show is being saved in the
+ * instance state.
+ */
+ @Test
+ @UiThreadTest
+ public void saveInstanceState_shouldSaveMaxNumberOfChildrenToShow() {
+ // No limit set, should save max value
+ PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ Parcelable state = mScreen.onSaveInstanceState();
+ assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+ assertEquals(Integer.MAX_VALUE,
+ ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+
+ // Has limit set, should save limit
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+ state = mScreen.onSaveInstanceState();
+ assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+ assertEquals(INITIAL_EXPANDED_COUNT,
+ ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+
+ // Preferences expanded already, should save max value
+ final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+ expandButton.performClick();
+ state = mScreen.onSaveInstanceState();
+ assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+ assertEquals(Integer.MAX_VALUE,
+ ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+ }
+
+ /**
+ * Verifies that if we restore to the same number of preferences to show, it will not update
+ * anything.
+ */
+ @Test
+ @UiThreadTest
+ public void restoreInstanceState_noChange_shouldDoNothing() {
+ Parcelable baseState = Preference.BaseSavedState.EMPTY_STATE;
+ // Initialized as expanded, restore with no saved data, should remain expanded
+ PreferenceGroupAdapter preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(baseState);
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+ // Initialized as collapsed, restore with no saved data, should remain collapsed
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(baseState);
+ assertPreferencesAreCollapsed(preferenceGroupAdapter);
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+ CollapsiblePreferenceGroupController.SavedState state =
+ new CollapsiblePreferenceGroupController.SavedState(baseState);
+ // Initialized as expanded, restore as expanded, should remain expanded
+ state.mMaxPreferenceToShow = Integer.MAX_VALUE;
+ mScreen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
+ preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(state);
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+ // Initialized as collapsed, restore as collapsed, should remain collapsed
+ state.mMaxPreferenceToShow = INITIAL_EXPANDED_COUNT;
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(state);
+ assertPreferencesAreCollapsed(preferenceGroupAdapter);
+ verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+ }
+
+ /**
+ * Verifies that if the children is collapsed previously, they should be collapsed after the
+ * state is being restored.
+ */
+ @Test
+ @UiThreadTest
+ public void restoreHierarchyState_previouslyCollapsed_shouldRestoreToCollapsedState() {
+ CollapsiblePreferenceGroupController.SavedState state =
+ new CollapsiblePreferenceGroupController.SavedState(
+ Preference.BaseSavedState.EMPTY_STATE);
+ // Initialized as expanded, restore as collapsed, should collapse
+ state.mMaxPreferenceToShow = INITIAL_EXPANDED_COUNT;
+ mScreen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
+ PreferenceGroupAdapter preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(state);
+ verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+ assertPreferencesAreCollapsed(preferenceGroupAdapter);
+ }
+
+ /**
+ * Verifies that if the children is expanded previously, they should be expanded after the
+ * state is being restored.
+ */
+ @Test
+ @UiThreadTest
+ public void restoreHierarchyState_previouslyExpanded_shouldRestoreToExpandedState() {
+ CollapsiblePreferenceGroupController.SavedState state =
+ new CollapsiblePreferenceGroupController.SavedState(
+ Preference.BaseSavedState.EMPTY_STATE);
+ // Initialized as collapsed, restore as expanded, should expand
+ state.mMaxPreferenceToShow = Integer.MAX_VALUE;
+ mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+ PreferenceGroupAdapter preferenceGroupAdapter =
+ PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+ mScreen.onRestoreInstanceState(state);
+ verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+ assertPreferencesAreExpanded(preferenceGroupAdapter);
+ }
+
+ // assert that the preferences are all expanded
+ private void assertPreferencesAreExpanded(PreferenceGroupAdapter adapter) {
+ assertEquals(TOTAL_PREFERENCE, adapter.getItemCount());
+ }
+
+ // assert that the preferences exceeding the limit are collapsed
+ private void assertPreferencesAreCollapsed(PreferenceGroupAdapter adapter) {
+ // list shows preferences up to the limit and the expand button
+ assertEquals(INITIAL_EXPANDED_COUNT + 1, adapter.getItemCount());
+ }
+
+ // create the number of preference in the corresponding preference group and add it to the cache
+ private void createTestPreferences(PreferenceGroup preferenceGroup,
+ List<Preference> preferenceList, int numPreference) {
+ for (int i = 0; i < numPreference; i++) {
+ final Preference preference = new Preference(mContext);
+ preference.setTitle(PREFERENCE_TITLE_PREFIX + i);
+ preferenceGroup.addPreference(preference);
+ preferenceList.add(preference);
+ }
+ }
+
+ // add a preference category and add the number of preference to it and the cache
+ private void createTestPreferencesCategory(PreferenceGroup preferenceGroup,
+ List<Preference> preferenceList, int numPreference) {
+ PreferenceCategory category = new PreferenceCategory(mContext);
+ preferenceGroup.addPreference(category);
+ preferenceList.add(category);
+ createTestPreferences(category, preferenceList, numPreference);
+ }
+
+}
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index 9b4500a..17cd472 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -55,6 +55,13 @@
method public void dispatchUpdatesTo(android.support.v7.util.ListUpdateCallback);
}
+ public static abstract class DiffUtil.ItemCallback<T> {
+ ctor public DiffUtil.ItemCallback();
+ method public abstract boolean areContentsTheSame(T, T);
+ method public abstract boolean areItemsTheSame(T, T);
+ method public java.lang.Object getChangePayload(T, T);
+ }
+
public abstract interface ListUpdateCallback {
method public abstract void onChanged(int, int, java.lang.Object);
method public abstract void onInserted(int, int);
@@ -99,6 +106,7 @@
method public abstract boolean areContentsTheSame(T2, T2);
method public abstract boolean areItemsTheSame(T2, T2);
method public abstract int compare(T2, T2);
+ method public java.lang.Object getChangePayload(T2, T2);
method public abstract void onChanged(int, int);
method public void onChanged(int, int, java.lang.Object);
}
@@ -305,6 +313,7 @@
method public android.support.v7.widget.RecyclerView.ViewHolder getChildViewHolder(android.view.View);
method public android.support.v7.widget.RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate();
method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
+ method public android.support.v7.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
method public android.support.v7.widget.RecyclerView.ItemAnimator getItemAnimator();
method public android.support.v7.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
method public int getItemDecorationCount();
@@ -339,6 +348,7 @@
method public void setAccessibilityDelegateCompat(android.support.v7.widget.RecyclerViewAccessibilityDelegate);
method public void setAdapter(android.support.v7.widget.RecyclerView.Adapter);
method public void setChildDrawingOrderCallback(android.support.v7.widget.RecyclerView.ChildDrawingOrderCallback);
+ method public void setEdgeEffectFactory(android.support.v7.widget.RecyclerView.EdgeEffectFactory);
method public void setHasFixedSize(boolean);
method public void setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator);
method public void setItemViewCacheSize(int);
@@ -417,6 +427,18 @@
method public abstract int onGetChildDrawingOrder(int, int);
}
+ public static class RecyclerView.EdgeEffectFactory {
+ ctor public RecyclerView.EdgeEffectFactory();
+ method protected android.widget.EdgeEffect createEdgeEffect(android.support.v7.widget.RecyclerView, int);
+ field public static final int DIRECTION_BOTTOM = 3; // 0x3
+ field public static final int DIRECTION_LEFT = 0; // 0x0
+ field public static final int DIRECTION_RIGHT = 2; // 0x2
+ field public static final int DIRECTION_TOP = 1; // 0x1
+ }
+
+ public static abstract class RecyclerView.EdgeEffectFactory.EdgeDirection implements java.lang.annotation.Annotation {
+ }
+
public static abstract class RecyclerView.ItemAnimator {
ctor public RecyclerView.ItemAnimator();
method public abstract boolean animateAppearance(android.support.v7.widget.RecyclerView.ViewHolder, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo);
diff --git a/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java b/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
index 6302666..ebc33f3 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
@@ -16,6 +16,7 @@
package android.support.v7.util;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
@@ -348,6 +349,72 @@
}
/**
+ * Callback for calculating the diff between two non-null items in a list.
+ * <p>
+ * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles
+ * just the second of these, which allows separation of code that indexes into an array or List
+ * from the presentation-layer and content specific diffing code.
+ *
+ * @param <T> Type of items to compare.
+ */
+ public abstract static class ItemCallback<T> {
+ /**
+ * Called to check whether two objects represent the same item.
+ * <p>
+ * For example, if your items have unique ids, this method should check their id equality.
+ *
+ * @param oldItem The item in the old list.
+ * @param newItem The item in the new list.
+ * @return True if the two items represent the same object or false if they are different.
+ *
+ * @see Callback#areItemsTheSame(int, int)
+ */
+ public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
+
+ /**
+ * Called to check whether two items have the same data.
+ * <p>
+ * This information is used to detect if the contents of an item have changed.
+ * <p>
+ * This method to check equality instead of {@link Object#equals(Object)} so that you can
+ * change its behavior depending on your UI.
+ * <p>
+ * For example, if you are using DiffUtil with a
+ * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
+ * return whether the items' visual representations are the same.
+ * <p>
+ * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for
+ * these items.
+ *
+ * @param oldItem The item in the old list.
+ * @param newItem The item in the new list.
+ * @return True if the contents of the items are the same or false if they are different.
+ *
+ * @see Callback#areContentsTheSame(int, int)
+ */
+ public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
+
+ /**
+ * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and
+ * {@link #areContentsTheSame(T, T)} returns false for them, this method is called to
+ * get a payload about the change.
+ * <p>
+ * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
+ * particular field that changed in the item and your
+ * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
+ * information to run the correct animation.
+ * <p>
+ * Default implementation returns {@code null}.
+ *
+ * @see Callback#getChangePayload(int, int)
+ */
+ @SuppressWarnings({"WeakerAccess", "unused"})
+ public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
+ return null;
+ }
+ }
+
+ /**
* Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
* add or remove operation. See the Myers' paper for details.
*/
diff --git a/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java b/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
index c62d0ce..af000a1 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
@@ -16,6 +16,8 @@
package android.support.v7.util;
+import android.support.annotation.Nullable;
+
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
@@ -315,7 +317,8 @@
newDataStart++;
mOldDataStart++;
if (!mCallback.areContentsTheSame(oldItem, newItem)) {
- mCallback.onChanged(mMergedSize - 1, 1);
+ mCallback.onChanged(mMergedSize - 1, 1,
+ mCallback.getChangePayload(oldItem, newItem));
}
} else {
// Old item is lower than or equal to (but not the same as the new). Output it.
@@ -401,7 +404,7 @@
return index;
} else {
mData[index] = item;
- mCallback.onChanged(index, 1);
+ mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
return index;
}
}
@@ -488,13 +491,13 @@
if (cmp == 0) {
mData[index] = item;
if (contentsChanged) {
- mCallback.onChanged(index, 1);
+ mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
}
return;
}
}
if (contentsChanged) {
- mCallback.onChanged(index, 1);
+ mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
}
// TODO this done in 1 pass to avoid shifting twice.
removeItemAtIndex(index, false);
@@ -741,6 +744,28 @@
* @return True if the two items represent the same object or false if they are different.
*/
abstract public boolean areItemsTheSame(T2 item1, T2 item2);
+
+ /**
+ * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and
+ * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this
+ * method to get a payload about the change.
+ * <p>
+ * For example, if you are using {@link Callback} with
+ * {@link android.support.v7.widget.RecyclerView}, you can return the particular field that
+ * changed in the item and your
+ * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
+ * information to run the correct animation.
+ * <p>
+ * Default implementation returns {@code null}.
+ *
+ * @param item1 The first item to check.
+ * @param item2 The second item to check.
+ * @return A payload object that represents the changes between the two items.
+ */
+ @Nullable
+ public Object getChangePayload(T2 item1, T2 item2) {
+ return null;
+ }
}
/**
@@ -801,6 +826,11 @@
}
@Override
+ public void onChanged(int position, int count, Object payload) {
+ mBatchingListUpdateCallback.onChanged(position, count, payload);
+ }
+
+ @Override
public boolean areContentsTheSame(T2 oldItem, T2 newItem) {
return mWrappedCallback.areContentsTheSame(oldItem, newItem);
}
@@ -810,6 +840,12 @@
return mWrappedCallback.areItemsTheSame(item1, item2);
}
+ @Nullable
+ @Override
+ public Object getChangePayload(T2 item1, T2 item2) {
+ return mWrappedCallback.getChangePayload(item1, item2);
+ }
+
/**
* This method dispatches any pending event notifications to the wrapped Callback.
* You <b>must</b> always call this method after you are done with editing the SortedList.
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
index cfa28e8..0c2da90 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
@@ -44,6 +44,7 @@
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.support.v4.os.TraceCompat;
+import android.support.v4.util.Preconditions;
import android.support.v4.view.AbsSavedState;
import android.support.v4.view.InputDeviceCompat;
import android.support.v4.view.MotionEventCompat;
@@ -385,8 +386,8 @@
private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
/**
- * Set to true when an adapter data set changed notification is received.
- * In that case, we cannot run any animations since we don't know what happened until layout.
+ * True after an event occurs that signals that the entire data set has changed. In that case,
+ * we cannot run any animations since we don't know what happened until layout.
*
* Attached items are invalid until next layout, at which point layout will animate/replace
* items as necessary, building up content from the (effectively) new adapter from scratch.
@@ -394,11 +395,20 @@
* Cached items must be discarded when setting this to true, so that the cache may be freely
* used by prefetching until the next layout occurs.
*
- * @see #setDataSetChangedAfterLayout()
+ * @see #processDataSetCompletelyChanged(boolean)
*/
boolean mDataSetHasChangedAfterLayout = false;
/**
+ * True after the data set has completely changed and
+ * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent
+ * measure/layout.
+ *
+ * @see #processDataSetCompletelyChanged(boolean)
+ */
+ boolean mDispatchItemsChangedEvent = false;
+
+ /**
* This variable is incremented during a dispatchLayout and/or scroll.
* Some methods should not be called during these periods (e.g. adapter data change).
* Doing so will create hard to find bugs so we better check it and throw an exception.
@@ -417,6 +427,8 @@
*/
private int mDispatchScrollCounter = 0;
+ @NonNull
+ private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -1041,6 +1053,7 @@
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
+ processDataSetCompletelyChanged(true);
requestLayout();
}
/**
@@ -1056,6 +1069,7 @@
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
+ processDataSetCompletelyChanged(false);
requestLayout();
}
@@ -1109,7 +1123,6 @@
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
- setDataSetChangedAfterLayout();
}
/**
@@ -2306,7 +2319,7 @@
if (mLeftGlow != null) {
return;
}
- mLeftGlow = new EdgeEffect(getContext());
+ mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT);
if (mClipToPadding) {
mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2319,7 +2332,7 @@
if (mRightGlow != null) {
return;
}
- mRightGlow = new EdgeEffect(getContext());
+ mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT);
if (mClipToPadding) {
mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2332,7 +2345,7 @@
if (mTopGlow != null) {
return;
}
- mTopGlow = new EdgeEffect(getContext());
+ mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
if (mClipToPadding) {
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2346,7 +2359,7 @@
if (mBottomGlow != null) {
return;
}
- mBottomGlow = new EdgeEffect(getContext());
+ mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM);
if (mClipToPadding) {
mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2360,6 +2373,32 @@
}
/**
+ * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}.
+ * <p>
+ * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared
+ * and new effects are created as needed using
+ * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)}
+ *
+ * @param edgeEffectFactory The {@link EdgeEffectFactory} instance.
+ */
+ public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) {
+ Preconditions.checkNotNull(edgeEffectFactory);
+ mEdgeEffectFactory = edgeEffectFactory;
+ invalidateGlows();
+ }
+
+ /**
+ * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing
+ * was set.
+ *
+ * @return The previously set {@link EdgeEffectFactory}
+ * @see #setEdgeEffectFactory(EdgeEffectFactory)
+ */
+ public EdgeEffectFactory getEdgeEffectFactory() {
+ return mEdgeEffectFactory;
+ }
+
+ /**
* Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
* in the Adapter but not visible in the UI), it employs a more involved focus search strategy
* that differs from other ViewGroups.
@@ -2480,9 +2519,17 @@
if (next == null || next == this) {
return false;
}
+ // panic, result view is not a child anymore, maybe workaround b/37864393
+ if (findContainingItemView(next) == null) {
+ return false;
+ }
if (focused == null) {
return true;
}
+ // panic, focused view is not a child anymore, maybe workaround b/37864393
+ if (findContainingItemView(focused) == null) {
+ return true;
+ }
mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
mTempRect2.set(0, 0, next.getWidth(), next.getHeight());
@@ -3369,7 +3416,9 @@
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
- mLayout.onItemsChanged(this);
+ if (mDispatchItemsChangedEvent) {
+ mLayout.onItemsChanged(this);
+ }
}
// simple animations are a subset of advanced animations (which will cause a
// pre-layout step)
@@ -3792,6 +3841,7 @@
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
+ mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
@@ -4259,19 +4309,21 @@
viewHolder.getUnmodifiedPayloads());
}
-
/**
- * Call this method to signal that *all* adapter content has changed (generally, because of
- * setAdapter, swapAdapter, or notifyDataSetChanged), and that once layout occurs, all
- * attached items should be discarded or animated.
+ * Processes the fact that, as far as we can tell, the data set has completely changed.
*
- * Attached items are labeled as invalid, and all cached items are discarded.
+ * <ul>
+ * <li>Once layout occurs, all attached items should be discarded or animated.
+ * <li>Attached items are labeled as invalid.
+ * <li>Because items may still be prefetched between a "data set completely changed"
+ * event and a layout event, all cached items are discarded.
+ * </ul>
*
- * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
- * so this method must always discard all cached views so that the only valid items that remain
- * in the cache, once layout occurs, are valid prefetched items.
+ * @param dispatchItemsChanged Whether to call
+ * {@link LayoutManager#onItemsChanged(RecyclerView)} during measure/layout.
*/
- void setDataSetChangedAfterLayout() {
+ void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
+ mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
markKnownViewsInvalid();
}
@@ -5081,7 +5133,7 @@
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
- setDataSetChangedAfterLayout();
+ processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
@@ -5130,6 +5182,46 @@
}
/**
+ * EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews.
+ *
+ * @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory)
+ */
+ public static class EdgeEffectFactory {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM})
+ public @interface EdgeDirection {}
+
+ /**
+ * Direction constant for the left edge
+ */
+ public static final int DIRECTION_LEFT = 0;
+
+ /**
+ * Direction constant for the top edge
+ */
+ public static final int DIRECTION_TOP = 1;
+
+ /**
+ * Direction constant for the right edge
+ */
+ public static final int DIRECTION_RIGHT = 2;
+
+ /**
+ * Direction constant for the bottom edge
+ */
+ public static final int DIRECTION_BOTTOM = 3;
+
+ /**
+ * Create a new EdgeEffect for the provided direction.
+ */
+ protected @NonNull EdgeEffect createEdgeEffect(RecyclerView view,
+ @EdgeDirection int direction) {
+ return new EdgeEffect(view.getContext());
+ }
+ }
+
+ /**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
* <p>
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
@@ -9422,9 +9514,11 @@
}
/**
- * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
- * The LayoutManager may use this opportunity to clear caches and configure state such
- * that it can relayout appropriately with the new data and potentially new view types.
+ * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
+ * {@link RecyclerView#setAdapter(Adapter)} or
+ * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this
+ * opportunity to clear caches and configure state such that it can relayout appropriately
+ * with the new data and potentially new view types.
*
* <p>The default implementation removes all currently attached views.</p>
*
@@ -9466,8 +9560,9 @@
}
/**
- * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
- * detailed information on what has actually changed.
+ * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or
+ * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire
+ * data set has changed.
*
* @param recyclerView
*/
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java b/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
index 4921541..a1203a6 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
@@ -56,4 +56,9 @@
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ mAdapter.notifyItemRangeChanged(position, count, payload);
+ }
}
diff --git a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
index 3ace217..bc50415 100644
--- a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
+++ b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
@@ -50,6 +50,16 @@
}
@Test
+ public void onChangeWithPayload() {
+ final Object payload = 7;
+ mBatchedCallback.onChanged(1, 2, payload);
+ verifyZeroInteractions(mMockCallback);
+ mBatchedCallback.dispatchLastEvent();
+ verify(mMockCallback).onChanged(1, 2, payload);
+ verifyNoMoreInteractions(mMockCallback);
+ }
+
+ @Test
public void onRemoved() {
mBatchedCallback.onRemoved(2, 3);
verifyZeroInteractions(mMockCallback);
diff --git a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
index da3c957..47d2ac0 100644
--- a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
+++ b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
@@ -16,6 +16,7 @@
package android.support.v7.util;
+import android.support.annotation.Nullable;
import android.support.test.filters.SmallTest;
import junit.framework.TestCase;
@@ -41,6 +42,8 @@
List<Pair> mRemovals = new ArrayList<Pair>();
List<Pair> mMoves = new ArrayList<Pair>();
List<Pair> mUpdates = new ArrayList<Pair>();
+ private boolean mPayloadChanges = false;
+ List<PayloadChange> mPayloadUpdates = new ArrayList<>();
private SortedList.Callback<Item> mCallback;
InsertedCallback<Item> mInsertedCallback;
ChangedCallback<Item> mChangedCallback;
@@ -97,6 +100,15 @@
}
@Override
+ public void onChanged(int position, int count, Object payload) {
+ if (mPayloadChanges) {
+ mPayloadUpdates.add(new PayloadChange(position, count, payload));
+ } else {
+ onChanged(position, count);
+ }
+ }
+
+ @Override
public boolean areContentsTheSame(Item oldItem, Item newItem) {
return oldItem.cmpField == newItem.cmpField && oldItem.data == newItem.data;
}
@@ -105,6 +117,15 @@
public boolean areItemsTheSame(Item item1, Item item2) {
return item1.id == item2.id;
}
+
+ @Nullable
+ @Override
+ public Object getChangePayload(Item item1, Item item2) {
+ if (mPayloadChanges) {
+ return item2.data;
+ }
+ return null;
+ }
};
mInsertedCallback = null;
mChangedCallback = null;
@@ -705,6 +726,76 @@
assertTrue(mAdditions.contains(new Pair(0, 6)));
}
+ @Test
+ public void testAddExistingItemCallsChangeWithPayload() {
+ mList.addAll(
+ new Item(1, 10),
+ new Item(2, 20),
+ new Item(3, 30)
+ );
+ mPayloadChanges = true;
+
+ // add an item with the same id but a new data field i.e. send an update
+ final Item twoUpdate = new Item(2, 20);
+ twoUpdate.data = 1337;
+ mList.add(twoUpdate);
+ assertEquals(1, mPayloadUpdates.size());
+ final PayloadChange update = mPayloadUpdates.get(0);
+ assertEquals(1, update.position);
+ assertEquals(1, update.count);
+ assertEquals(1337, update.payload);
+ assertEquals(3, size());
+ }
+
+ @Test
+ public void testUpdateItemCallsChangeWithPayload() {
+ mList.addAll(
+ new Item(1, 10),
+ new Item(2, 20),
+ new Item(3, 30)
+ );
+ mPayloadChanges = true;
+
+ // add an item with the same id but a new data field i.e. send an update
+ final Item twoUpdate = new Item(2, 20);
+ twoUpdate.data = 1337;
+ mList.updateItemAt(1, twoUpdate);
+ assertEquals(1, mPayloadUpdates.size());
+ final PayloadChange update = mPayloadUpdates.get(0);
+ assertEquals(1, update.position);
+ assertEquals(1, update.count);
+ assertEquals(1337, update.payload);
+ assertEquals(3, size());
+ assertEquals(1337, mList.get(1).data);
+ }
+
+ @Test
+ public void testAddMultipleExistingItemCallsChangeWithPayload() {
+ mList.addAll(
+ new Item(1, 10),
+ new Item(2, 20),
+ new Item(3, 30)
+ );
+ mPayloadChanges = true;
+
+ // add two items with the same ids but a new data fields i.e. send two updates
+ final Item twoUpdate = new Item(2, 20);
+ twoUpdate.data = 222;
+ final Item threeUpdate = new Item(3, 30);
+ threeUpdate.data = 333;
+ mList.addAll(twoUpdate, threeUpdate);
+ assertEquals(2, mPayloadUpdates.size());
+ final PayloadChange update1 = mPayloadUpdates.get(0);
+ assertEquals(1, update1.position);
+ assertEquals(1, update1.count);
+ assertEquals(222, update1.payload);
+ final PayloadChange update2 = mPayloadUpdates.get(1);
+ assertEquals(2, update2.position);
+ assertEquals(1, update2.count);
+ assertEquals(333, update2.payload);
+ assertEquals(3, size());
+ }
+
private int size() {
return mList.size();
}
@@ -821,4 +912,37 @@
return result;
}
}
+
+ private static final class PayloadChange {
+ public final int position;
+ public final int count;
+ public final Object payload;
+
+ PayloadChange(int position, int count, Object payload) {
+ this.position = position;
+ this.count = count;
+ this.payload = payload;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PayloadChange payloadChange = (PayloadChange) o;
+
+ if (position != payloadChange.position) return false;
+ if (count != payloadChange.count) return false;
+ return payload != null ? payload.equals(payloadChange.payload)
+ : payloadChange.payload == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = position;
+ result = 31 * result + count;
+ result = 31 * result + (payload != null ? payload.hashCode() : 0);
+ return result;
+ }
+ }
}
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
index f4caad3..32e1295 100644
--- a/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
+++ b/v7/recyclerview/tests/src/android/support/v7/util/ImeCleanUpTestRule.java
@@ -16,8 +16,12 @@
package android.support.v7.util;
+import android.app.Instrumentation;
import android.graphics.Rect;
+import android.support.annotation.NonNull;
import android.support.testutils.PollingCheck;
+import android.support.v7.widget.TestActivity;
+import android.view.KeyEvent;
import android.view.View;
import org.junit.rules.TestRule;
@@ -25,13 +29,22 @@
import org.junit.runners.model.Statement;
/**
- * A JUnit rule that ensures that IME is closed after a test is finished (or exception thrown).
- * A test that triggers IME open/close should call setContainerView with the activity's container
- * view in order to trigger this cleanup at the end of the test. Otherwise, no cleanup happens.
+ * A JUnit rule that ensures that IME is closed after a test is finished by determining if the
+ * keyboard is open, and if it is closing it. If the rules determines the keyboard is open and is
+ * unable to close it within a timeout (see source), an exception will be thrown.
+ *
+ * A test that wants to benefit from this functionality must call
+ * {@link #setup(TestActivity, Instrumentation)} with the {@link TestActivity} under test and the
+ * test's {@link Instrumentation}, or this rule does nothing.
*/
public class ImeCleanUpTestRule implements TestRule {
+ // We consider the keyboard open if its height is at least this percentage of the available
+ // screen height.
+ private static final float KEYBOARD_HEIGHT_TO_SCREEN_RATIO = .15f;
+
private View mContainerView;
+ private Instrumentation mInstrumentation;
@Override
public Statement apply(final Statement base, Description description) {
@@ -48,39 +61,41 @@
}
/**
- * Sets the container view used to calculate the total screen height and the height available
- * to the test activity.
+ * Call to enable the functionality of this TestRule.
+ * @param testActivity The {@link TestActivity} under test.
+ * @param instrumentation The test's {@link Instrumentation}.
*/
- public void setContainerView(View containerView) {
- mContainerView = containerView;
+ public void setup(@NonNull TestActivity testActivity,
+ @NonNull Instrumentation instrumentation) {
+ mContainerView = testActivity.getContainer();
+ mInstrumentation = instrumentation;
}
private void closeImeIfOpen() {
- if (mContainerView == null) {
+ if (mContainerView == null || mInstrumentation == null) {
return;
}
- // Ensuring that IME is closed after starting each test.
+
final Rect r = new Rect();
mContainerView.getWindowVisibleDisplayFrame(r);
+
// This is the entire height of the screen available to both the view and IME
- final int screenHeight = mContainerView.getRootView().getHeight();
+ final int screenHeight = mContainerView.getHeight();
// r.bottom is the position above IME if it's open or device button.
// if IME is shown, r.bottom is smaller than screenHeight.
int imeHeight = screenHeight - r.bottom;
- // Picking a threshold to detect when IME is open
- if (imeHeight > screenHeight * 0.15) {
- // Soft keyboard is shown, will wait for it to close after running the test. Note that
- // we don't press back button here as the IME should close by itself when a test
- // finishes. If the wait isn't done here, the IME can mess up with the layout of the
- // next test.
+ if (imeHeight > screenHeight * KEYBOARD_HEIGHT_TO_SCREEN_RATIO) {
+ // Soft keyboard is shown, therefore we click the back button to close it and wait for
+ // it to be closed.
+ mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
@Override
public boolean canProceed() {
mContainerView.getWindowVisibleDisplayFrame(r);
int imeHeight = screenHeight - r.bottom;
- return imeHeight < screenHeight * 0.15;
+ return imeHeight < screenHeight * KEYBOARD_HEIGHT_TO_SCREEN_RATIO;
}
});
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
index 1a64e3c..418d33f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
+++ b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
@@ -154,6 +154,18 @@
inst.waitForIdleSync();
}
+ public static void scrollView(int axis, int axisValue, int inputDevice, View v) {
+ MotionEvent.PointerProperties[] pointerProperties = { new MotionEvent.PointerProperties() };
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.setAxisValue(axis, axisValue);
+ MotionEvent.PointerCoords[] pointerCoords = { coords };
+ MotionEvent e = MotionEvent.obtain(
+ 0, System.currentTimeMillis(), MotionEvent.ACTION_SCROLL,
+ 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputDevice, 0);
+ v.onGenericMotionEvent(e);
+ e.recycle();
+ }
+
public static void dragViewToTop(Instrumentation inst, View v) {
dragViewToTop(inst, v, calculateStepsForDistance(v.getTop()));
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
new file mode 100644
index 0000000..0644416
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+
+import static android.support.v7.widget.RecyclerView.EdgeEffectFactory;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.view.InputDeviceCompat;
+import android.support.v7.util.TouchUtils;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.EdgeEffect;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests custom edge effect are properly applied when scrolling.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CustomEdgeEffectTest extends BaseRecyclerViewInstrumentationTest {
+
+ private static final int NUM_ITEMS = 10;
+
+ private LinearLayoutManager mLayoutManager;
+ private RecyclerView mRecyclerView;
+
+ @Before
+ public void setup() throws Throwable {
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ mLayoutManager.ensureLayoutState();
+
+ mRecyclerView = new RecyclerView(getActivity());
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+ holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+ return holder;
+ }
+ });
+ setRecyclerView(mRecyclerView);
+ getInstrumentation().waitForIdleSync();
+ assertThat("Test sanity", mRecyclerView.getChildCount() > 0, is(true));
+ }
+
+ @Test
+ public void testEdgeEffectDirections() throws Throwable {
+ TestEdgeEffectFactory factory = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory);
+ scrollToPosition(0);
+ waitForIdleScroll(mRecyclerView);
+ scrollViewBy(3);
+ assertNull(factory.mBottom);
+ assertNotNull(factory.mTop);
+ assertTrue(factory.mTop.mPullDistance > 0);
+
+ scrollToPosition(NUM_ITEMS - 1);
+ waitForIdleScroll(mRecyclerView);
+ scrollViewBy(-3);
+
+ assertNotNull(factory.mBottom);
+ assertTrue(factory.mBottom.mPullDistance > 0);
+ }
+
+ @Test
+ public void testEdgeEffectReplaced() throws Throwable {
+ TestEdgeEffectFactory factory1 = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory1);
+ scrollToPosition(0);
+ waitForIdleScroll(mRecyclerView);
+
+ scrollViewBy(3);
+ assertNotNull(factory1.mTop);
+ float oldPullDistance = factory1.mTop.mPullDistance;
+
+ waitForIdleScroll(mRecyclerView);
+ TestEdgeEffectFactory factory2 = new TestEdgeEffectFactory();
+ mRecyclerView.setEdgeEffectFactory(factory2);
+ scrollViewBy(30);
+ assertNotNull(factory2.mTop);
+
+ assertTrue(factory2.mTop.mPullDistance > oldPullDistance);
+ assertEquals(oldPullDistance, factory1.mTop.mPullDistance, 0.1f);
+ }
+
+ private void scrollViewBy(final int value) throws Throwable {
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value,
+ InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
+ }
+ });
+ }
+
+ private class TestEdgeEffectFactory extends EdgeEffectFactory {
+
+ TestEdgeEffect mTop, mBottom;
+
+ @NonNull
+ @Override
+ protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
+ TestEdgeEffect effect = new TestEdgeEffect(view.getContext());
+ if (direction == EdgeEffectFactory.DIRECTION_TOP) {
+ mTop = effect;
+ } else if (direction == EdgeEffectFactory.DIRECTION_BOTTOM) {
+ mBottom = effect;
+ }
+ return effect;
+ }
+ }
+
+ private class TestEdgeEffect extends EdgeEffect {
+
+ private float mPullDistance;
+
+ TestEdgeEffect(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onPull(float deltaDistance, float displacement) {
+ onPull(deltaDistance);
+ }
+
+ @Override
+ public void onPull(float deltaDistance) {
+ mPullDistance = deltaDistance;
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index 23eaf52..5d378ff 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -184,7 +184,7 @@
final int spanCount = 3;
final int itemCount = 100;
- imeCleanUp.setContainerView(getActivity().getContainer());
+ imeCleanUp.setup(getActivity(), getInstrumentation());
RecyclerView recyclerView = new WrappedRecyclerView(getActivity());
GridEditTextAdapter editTextAdapter = new GridEditTextAdapter(itemCount) {
@Override
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 91d0dbf..4dd0d8f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -82,7 +82,7 @@
// The condition for this test is setting RV's height to a non-exact height, so that measure
// is called twice (once with the larger height and another time with smaller height when
// the keyboard shows up). To ensure this resizing of RV, SOFT_INPUT_ADJUST_RESIZE is set.
- imeCleanUp.setContainerView(getActivity().getContainer());
+ imeCleanUp.setup(getActivity(), getInstrumentation());
final LinearLayout container = new LinearLayout(getActivity());
container.setOrientation(LinearLayout.VERTICAL);
container.setLayoutParams(
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index 5946940..3357c2f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -477,7 +477,6 @@
assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName()));
assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName()));
assertTrue("must contain ctx class", m.contains(getContext().getClass().getName()));
-
}
}
@@ -488,20 +487,71 @@
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
measure();
layout();
- assertSame(focusAdapter.mBottomLeft,
- focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
- assertSame(focusAdapter.mBottomRight,
- focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
+ boolean isIcsOrLower = Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1;
+
+ // On API 15 and lower, focus forward get's translated to focus down.
+ View expected = isIcsOrLower ? focusAdapter.mBottomRight : focusAdapter.mBottomLeft;
+ assertEquals(expected, focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
+
+ // On API 15 and lower, focus forward get's translated to focus down, which in this case
+ // runs out of the RecyclerView, thus returning null.
+ expected = isIcsOrLower ? null : focusAdapter.mBottomRight;
+ assertSame(expected, focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
// we don't want looping within RecyclerView
assertNull(focusAdapter.mBottomRight.focusSearch(View.FOCUS_FORWARD));
assertNull(focusAdapter.mTopLeft.focusSearch(View.FOCUS_BACKWARD));
}
+ @Test
+ public void setAdapter_callsCorrectLmMethods() throws Throwable {
+ MockLayoutManager mockLayoutManager = new MockLayoutManager();
+ MockAdapter mockAdapter = new MockAdapter(1);
+ mRecyclerView.setLayoutManager(mockLayoutManager);
+
+ mRecyclerView.setAdapter(mockAdapter);
+ layout();
+
+ assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+ assertEquals(0, mockLayoutManager.mItemsChangedCount);
+ }
+
+ @Test
+ public void swapAdapter_callsCorrectLmMethods() throws Throwable {
+ MockLayoutManager mockLayoutManager = new MockLayoutManager();
+ MockAdapter mockAdapter = new MockAdapter(1);
+ mRecyclerView.setLayoutManager(mockLayoutManager);
+
+ mRecyclerView.swapAdapter(mockAdapter, true);
+ layout();
+
+ assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+ assertEquals(1, mockLayoutManager.mItemsChangedCount);
+ }
+
+ @Test
+ public void notifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+ MockLayoutManager mockLayoutManager = new MockLayoutManager();
+ MockAdapter mockAdapter = new MockAdapter(1);
+ mRecyclerView.setLayoutManager(mockLayoutManager);
+ mRecyclerView.setAdapter(mockAdapter);
+ mockLayoutManager.mAdapterChangedCount = 0;
+ mockLayoutManager.mItemsChangedCount = 0;
+
+ mockAdapter.notifyDataSetChanged();
+ layout();
+
+ assertEquals(0, mockLayoutManager.mAdapterChangedCount);
+ assertEquals(1, mockLayoutManager.mItemsChangedCount);
+ }
+
static class MockLayoutManager extends RecyclerView.LayoutManager {
int mLayoutCount = 0;
int mAdapterChangedCount = 0;
+ int mItemsChangedCount = 0;
RecyclerView.Adapter mPrevAdapter;
@@ -519,6 +569,11 @@
}
@Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ mItemsChangedCount++;
+ }
+
+ @Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
mLayoutCount += 1;
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index bdc450b..1c03c0f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -253,6 +253,186 @@
}
@Test
+ public void setAdapter_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ rv.swapAdapter(testAdapter, true);
+ rv.setAdapter(testAdapter);
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(2, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
+ public void setAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyDataSetChanged();
+ rv.setAdapter(testAdapter);
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(1, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
+ public void notifyDataSetChanged_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ rv.setAdapter(testAdapter);
+ testAdapter.notifyDataSetChanged();
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(1, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
+ public void notifyDataSetChanged_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ rv.swapAdapter(testAdapter, true);
+ testAdapter.notifyDataSetChanged();
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(1, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
+ public void swapAdapter_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ rv.setAdapter(testAdapter);
+ rv.swapAdapter(testAdapter, true);
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(2, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
+ public void swapAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+ final RecyclerView rv = new RecyclerView(getActivity());
+ final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+ final TestAdapter testAdapter = new TestAdapter(1);
+
+ lm.expectLayouts(1);
+ rv.setLayoutManager(lm);
+ setRecyclerView(rv);
+ setAdapter(testAdapter);
+ lm.waitForLayout(2);
+
+ lm.onAdapterChagnedCallCount = 0;
+ lm.onItemsChangedCallCount = 0;
+
+ lm.expectLayouts(1);
+ mActivityRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyDataSetChanged();
+ rv.swapAdapter(testAdapter, true);
+ }
+ });
+ lm.waitForLayout(2);
+
+ assertEquals(1, lm.onAdapterChagnedCallCount);
+ assertEquals(1, lm.onItemsChangedCallCount);
+ }
+
+ @Test
public void setAdapterNotifyItemRangeInsertedCrashTest() throws Throwable {
final RecyclerView rv = new RecyclerView(getActivity());
final TestLayoutManager lm = new LayoutAllLayoutManager(true);
@@ -4788,6 +4968,8 @@
public class LayoutAllLayoutManager extends TestLayoutManager {
private final boolean mAllowNullLayoutLatch;
+ public int onItemsChangedCallCount = 0;
+ public int onAdapterChagnedCallCount = 0;
public LayoutAllLayoutManager() {
// by default, we don't allow unexpected layouts.
@@ -4797,6 +4979,18 @@
mAllowNullLayoutLatch = allowNullLayoutLatch;
}
+ @Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ super.onItemsChanged(recyclerView);
+ onItemsChangedCallCount++;
+ }
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+ RecyclerView.Adapter newAdapter) {
+ super.onAdapterChanged(oldAdapter, newAdapter);
+ onAdapterChagnedCallCount++;
+ }
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
index aee15dd..2f80156 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
@@ -24,8 +24,8 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.view.InputDeviceCompat;
-import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v7.util.TouchUtils;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -60,9 +60,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
}
@@ -73,9 +72,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, false);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
}
@@ -84,9 +82,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
}
@@ -95,9 +92,8 @@
MockLayoutManager layoutManager = new MockLayoutManager(true, true);
mRecyclerView.setLayoutManager(layoutManager);
layout();
- MotionEvent e = obtainScrollMotionEvent(
- MotionEventCompat.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
- mRecyclerView.onGenericMotionEvent(e);
+ TouchUtils.scrollView(
+ MotionEvent.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
}
diff --git a/wear/api/current.txt b/wear/api/current.txt
index e397eb3..3f148d3 100644
--- a/wear/api/current.txt
+++ b/wear/api/current.txt
@@ -2,7 +2,7 @@
public final class AmbientMode extends android.app.Fragment {
ctor public AmbientMode();
- method public static <T extends android.app.Activity & android.support.wear.ambient.AmbientMode.AmbientCallbackProvider> android.support.wear.ambient.AmbientMode.AmbientController attachAmbientSupport(T);
+ method public static <T extends android.app.Activity> android.support.wear.ambient.AmbientMode.AmbientController attachAmbientSupport(T);
field public static final java.lang.String EXTRA_BURN_IN_PROTECTION = "com.google.android.wearable.compat.extra.BURN_IN_PROTECTION";
field public static final java.lang.String EXTRA_LOWBIT_AMBIENT = "com.google.android.wearable.compat.extra.LOWBIT_AMBIENT";
field public static final java.lang.String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
@@ -21,7 +21,6 @@
public final class AmbientMode.AmbientController {
method public boolean isAmbient();
- method public void setAutoResumeEnabled(boolean);
}
}
diff --git a/wear/res/drawable-v21/ws_ic_expand_more_white_22.xml b/wear/res/drawable-v23/ws_ic_expand_more_white_22.xml
similarity index 100%
rename from wear/res/drawable-v21/ws_ic_expand_more_white_22.xml
rename to wear/res/drawable-v23/ws_ic_expand_more_white_22.xml
diff --git a/wear/res/drawable-v21/ws_switch_thumb_material_anim.xml b/wear/res/drawable-v23/ws_switch_thumb_material_anim.xml
similarity index 100%
rename from wear/res/drawable-v21/ws_switch_thumb_material_anim.xml
rename to wear/res/drawable-v23/ws_switch_thumb_material_anim.xml
diff --git a/wear/res/values-v20/styles.xml b/wear/res/values-v20/styles.xml
deleted file mode 100644
index 92613f2..0000000
--- a/wear/res/values-v20/styles.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
-
- <style name="WsPageIndicatorViewStyle">
- <item name="wsPageIndicatorDotSpacing">7.8dp</item>
- <item name="wsPageIndicatorDotRadius">2.1dp</item>
- <item name="wsPageIndicatorDotRadiusSelected">3.1dp</item>
- <item name="wsPageIndicatorDotColor">?android:attr/colorForeground</item>
- <item name="wsPageIndicatorDotColorSelected">?android:attr/colorForeground</item>
- <item name="wsPageIndicatorDotFadeOutDelay">1000</item>
- <item name="wsPageIndicatorDotFadeOutDuration">250</item>
- <item name="wsPageIndicatorDotFadeInDuration">100</item>
- <item name="wsPageIndicatorDotFadeWhenIdle">true</item>
- <item name="wsPageIndicatorDotShadowColor">#66000000</item>
- <item name="wsPageIndicatorDotShadowRadius">1dp</item>
- <item name="wsPageIndicatorDotShadowDx">0.5dp</item>
- <item name="wsPageIndicatorDotShadowDy">0.5dp</item>
- </style>
-
-</resources>
diff --git a/wear/res/values-v23/styles.xml b/wear/res/values-v23/styles.xml
index 6bb1a51..63ed2d8 100644
--- a/wear/res/values-v23/styles.xml
+++ b/wear/res/values-v23/styles.xml
@@ -14,6 +14,22 @@
limitations under the License.
-->
<resources>
+ <style name="WsPageIndicatorViewStyle">
+ <item name="wsPageIndicatorDotSpacing">7.8dp</item>
+ <item name="wsPageIndicatorDotRadius">2.1dp</item>
+ <item name="wsPageIndicatorDotRadiusSelected">3.1dp</item>
+ <item name="wsPageIndicatorDotColor">?android:attr/colorForeground</item>
+ <item name="wsPageIndicatorDotColorSelected">?android:attr/colorForeground</item>
+ <item name="wsPageIndicatorDotFadeOutDelay">1000</item>
+ <item name="wsPageIndicatorDotFadeOutDuration">250</item>
+ <item name="wsPageIndicatorDotFadeInDuration">100</item>
+ <item name="wsPageIndicatorDotFadeWhenIdle">true</item>
+ <item name="wsPageIndicatorDotShadowColor">#66000000</item>
+ <item name="wsPageIndicatorDotShadowRadius">1dp</item>
+ <item name="wsPageIndicatorDotShadowDx">0.5dp</item>
+ <item name="wsPageIndicatorDotShadowDy">0.5dp</item>
+ </style>
+
<style name="WsWearableActionDrawerItemText">
<item name="android:layout_gravity">center_vertical</item>
<item name="android:ellipsize">end</item>
diff --git a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
index 1911a40..0077a5b 100644
--- a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
+++ b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
@@ -21,7 +21,9 @@
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.util.Log;
import com.google.android.wearable.compat.WearableActivityController;
@@ -38,7 +40,7 @@
* It should be called with an {@link Activity} as an argument and that {@link Activity} will then
* be able to receive ambient lifecycle events through an {@link AmbientCallback}. The
* {@link Activity} will also receive a {@link AmbientController} object from the attachment which
- * can be used to query the current status of the ambient mode, or toggle simple settings.
+ * can be used to query the current status of the ambient mode.
* An example of how to attach {@link AmbientMode} to your {@link Activity} and use
* the {@link AmbientController} can be found below:
* <p>
@@ -48,6 +50,7 @@
* }</pre>
*/
public final class AmbientMode extends Fragment {
+ private static final String TAG = "AmbientMode";
/**
* Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
@@ -104,9 +107,6 @@
* running (after onResume, before onPause). All drawing should complete by the conclusion
* of this method. Note that {@code invalidate()} calls will be executed before resuming
* lower-power mode.
- * <p>
- * <p><em>Derived classes must call through to the super class's implementation of this
- * method. If they do not, an exception will be thrown.</em>
*
* @param ambientDetails bundle containing information about the display being used.
* It includes information about low-bit color and burn-in protection.
@@ -117,36 +117,40 @@
* Called when the system is updating the display for ambient mode. Activities may use this
* opportunity to update or invalidate views.
*/
- public void onUpdateAmbient() {};
+ public void onUpdateAmbient() {}
/**
* Called when an activity should exit ambient mode. This event is sent while an activity is
* running (after onResume, before onPause).
- * <p>
- * <p><em>Derived classes must call through to the super class's implementation of this
- * method. If they do not, an exception will be thrown.</em>
*/
- public void onExitAmbient() {};
+ public void onExitAmbient() {}
}
private final AmbientDelegate.AmbientCallback mCallback =
new AmbientDelegate.AmbientCallback() {
@Override
public void onEnterAmbient(Bundle ambientDetails) {
- mSuppliedCallback.onEnterAmbient(ambientDetails);
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onEnterAmbient(ambientDetails);
+ }
}
@Override
public void onExitAmbient() {
- mSuppliedCallback.onExitAmbient();
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onExitAmbient();
+ }
}
@Override
public void onUpdateAmbient() {
- mSuppliedCallback.onUpdateAmbient();
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onUpdateAmbient();
+ }
}
};
private AmbientDelegate mDelegate;
+ @Nullable
private AmbientCallback mSuppliedCallback;
private AmbientController mController;
@@ -166,8 +170,7 @@
if (context instanceof AmbientCallbackProvider) {
mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback();
} else {
- throw new IllegalArgumentException(
- "fragment should attach to an activity that implements AmbientCallback");
+ Log.w(TAG, "No callback provided - enabling only smart resume");
}
}
@@ -176,7 +179,9 @@
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate();
- mDelegate.setAmbientEnabled();
+ if (mSuppliedCallback != null) {
+ mDelegate.setAmbientEnabled();
+ }
}
@Override
@@ -215,15 +220,19 @@
}
/**
- * Attach ambient support to the given activity.
+ * Attach ambient support to the given activity. Calling this method with an Activity
+ * implementing the {@link AmbientCallbackProvider} interface will provide you with an
+ * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively,
+ * you can call this method with an Activity which does not implement
+ * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume
+ * functionality. This is equivalent to providing (@code null} from
+ * the {@link AmbientCallbackProvider}.
*
- * @param activity the activity to attach ambient support to. This activity has to also
- * implement {@link AmbientCallbackProvider}
+ * @param activity the activity to attach ambient support to.
* @return the associated {@link AmbientController} which can be used to query the state of
- * ambient mode and toggle simple settings related to it.
+ * ambient mode.
*/
- public static <T extends Activity & AmbientCallbackProvider> AmbientController
- attachAmbientSupport(T activity) {
+ public static <T extends Activity> AmbientController attachAmbientSupport(T activity) {
FragmentManager fragmentManager = activity.getFragmentManager();
AmbientMode ambientFragment = (AmbientMode) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (ambientFragment == null) {
@@ -251,9 +260,8 @@
/**
* A class for interacting with the ambient mode on a wearable device. This class can be used to
- * query the current state of ambient mode and to enable or disable certain settings.
- * An instance of this class is returned to the user when they attach their {@link Activity}
- * to {@link AmbientMode}.
+ * query the current state of ambient mode. An instance of this class is returned to the user
+ * when they attach their {@link Activity} to {@link AmbientMode}.
*/
public final class AmbientController {
private static final String TAG = "AmbientController";
diff --git a/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java b/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
index cd90a3b..9421d9e 100644
--- a/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
+++ b/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
@@ -16,7 +16,6 @@
package android.support.wear.ambient;
import android.os.Build;
-import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import com.google.android.wearable.WearableSharedLib;
@@ -24,10 +23,7 @@
/**
* Internal class which can be used to determine the version of the wearable shared library that is
* available on the current device.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
final class SharedLibraryVersion {
private SharedLibraryVersion() {
diff --git a/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java b/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
index 1682dc0..4b6ae8e 100644
--- a/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
+++ b/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
@@ -28,7 +28,7 @@
*
* @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class WearableControllerProvider {
private static final String TAG = "WearableControllerProvider";
diff --git a/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java b/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
index f23a688..8ba3adf 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
@@ -26,7 +26,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public final class ResourcesUtil {
/**
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
index ad56048..4a7ce66 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
@@ -28,7 +28,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class MultiPagePresenter extends WearableNavigationDrawerPresenter {
private final Ui mUi;
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
index 9056845..0ba2f5d 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
@@ -16,6 +16,7 @@
package android.support.wear.internal.widget.drawer;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
@@ -37,7 +38,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class MultiPageUi implements MultiPagePresenter.Ui {
private static final String TAG = "MultiPageUi";
@@ -62,13 +63,8 @@
final View content = inflater.inflate(R.layout.ws_navigation_drawer_view, drawer,
false /* attachToRoot */);
- mNavigationPager =
- (ViewPager) content
- .findViewById(R.id.ws_navigation_drawer_view_pager);
- mPageIndicatorView =
- (PageIndicatorView)
- content.findViewById(
- R.id.ws_navigation_drawer_page_indicator);
+ mNavigationPager = content.findViewById(R.id.ws_navigation_drawer_view_pager);
+ mPageIndicatorView = content.findViewById(R.id.ws_navigation_drawer_page_indicator);
drawer.setDrawerContent(content);
}
@@ -132,8 +128,9 @@
mAdapter = adapter;
}
+ @NonNull
@Override
- public Object instantiateItem(ViewGroup container, int position) {
+ public Object instantiateItem(@NonNull ViewGroup container, int position) {
// Do not attach to root in the inflate method. The view needs to returned at the end
// of this method. Attaching to root will cause view to point to container instead.
final View view =
@@ -141,17 +138,17 @@
.inflate(R.layout.ws_navigation_drawer_item_view, container, false);
container.addView(view);
final ImageView iconView =
- (ImageView) view
- .findViewById(R.id.ws_navigation_drawer_item_icon);
+ view.findViewById(R.id.ws_navigation_drawer_item_icon);
final TextView textView =
- (TextView) view.findViewById(R.id.ws_navigation_drawer_item_text);
+ view.findViewById(R.id.ws_navigation_drawer_item_text);
iconView.setImageDrawable(mAdapter.getItemDrawable(position));
textView.setText(mAdapter.getItemText(position));
return view;
}
@Override
- public void destroyItem(ViewGroup container, int position, Object object) {
+ public void destroyItem(@NonNull ViewGroup container, int position,
+ @NonNull Object object) {
container.removeView((View) object);
}
@@ -161,12 +158,12 @@
}
@Override
- public int getItemPosition(Object object) {
+ public int getItemPosition(@NonNull Object object) {
return POSITION_NONE;
}
@Override
- public boolean isViewFromObject(View view, Object object) {
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
}
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
index d90b589..42cc7d0 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
@@ -29,7 +29,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class SinglePagePresenter extends WearableNavigationDrawerPresenter {
private static final long DRAWER_CLOSE_DELAY_MS = 500;
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
index f3a4290..ffc966f 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
@@ -38,7 +38,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class SinglePageUi implements SinglePagePresenter.Ui {
@IdRes
@@ -111,11 +111,10 @@
R.layout.ws_single_page_nav_drawer_peek_view, mDrawer,
false /* attachToRoot */);
- mTextView = (TextView) content.findViewById(R.id.ws_nav_drawer_text);
+ mTextView = content.findViewById(R.id.ws_nav_drawer_text);
mSinglePageImageViews = new CircledImageView[count];
for (int i = 0; i < count; i++) {
- mSinglePageImageViews[i] = (CircledImageView) content
- .findViewById(SINGLE_PAGE_BUTTON_IDS[i]);
+ mSinglePageImageViews[i] = content.findViewById(SINGLE_PAGE_BUTTON_IDS[i]);
mSinglePageImageViews[i].setOnClickListener(new OnSelectedClickHandler(i, mPresenter));
mSinglePageImageViews[i].setCircleHidden(true);
}
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
index 1c8c4fb..df108aa 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
@@ -30,7 +30,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public abstract class WearableNavigationDrawerPresenter {
private final Set<OnItemSelectedListener> mOnItemSelectedListeners = new HashSet<>();
diff --git a/wear/src/main/java/android/support/wear/utils/MetadataConstants.java b/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
index 5be9c52..c7335c2 100644
--- a/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
+++ b/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
@@ -15,16 +15,13 @@
*/
package android.support.wear.utils;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.os.Build;
/**
* Constants for android wear apps which are related to manifest meta-data.
*/
-@TargetApi(Build.VERSION_CODES.N)
public class MetadataConstants {
// Constants for standalone apps. //
diff --git a/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java b/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
index 131bae8..9c56a83 100644
--- a/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
+++ b/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
@@ -17,8 +17,6 @@
package android.support.wear.widget;
import android.animation.TimeInterpolator;
-import android.annotation.TargetApi;
-import android.os.Build;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
@@ -27,8 +25,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
-@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
+@RestrictTo(Scope.LIBRARY)
class BezierSCurveInterpolator implements TimeInterpolator {
/**
diff --git a/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java b/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
index ba35f2c..a8b1381 100644
--- a/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
@@ -20,7 +20,6 @@
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -111,21 +110,6 @@
}
@Override
- public WindowInsets onApplyWindowInsets(WindowInsets insets) {
- insets = super.onApplyWindowInsets(insets);
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- final boolean round = insets.isRound();
- if (round != mIsRound) {
- mIsRound = round;
- requestLayout();
- }
- mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
- insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
- }
- return insets;
- }
-
- @Override
public void setForeground(Drawable drawable) {
super.setForeground(drawable);
mForegroundDrawable = drawable;
@@ -145,14 +129,10 @@
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- requestApplyInsets();
- } else {
- mIsRound = getResources().getConfiguration().isScreenRound();
- WindowInsets insets = getRootWindowInsets();
- mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
- insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
- }
+ mIsRound = getResources().getConfiguration().isScreenRound();
+ WindowInsets insets = getRootWindowInsets();
+ mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
+ insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
}
@Override
@@ -413,7 +393,7 @@
public static class LayoutParams extends FrameLayout.LayoutParams {
/** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef({BOX_NONE, BOX_LEFT, BOX_TOP, BOX_RIGHT, BOX_BOTTOM, BOX_ALL})
@Retention(RetentionPolicy.SOURCE)
public @interface BoxedEdges {}
diff --git a/wear/src/main/java/android/support/wear/widget/CircledImageView.java b/wear/src/main/java/android/support/wear/widget/CircledImageView.java
index 03ed8c9..c441dd5 100644
--- a/wear/src/main/java/android/support/wear/widget/CircledImageView.java
+++ b/wear/src/main/java/android/support/wear/widget/CircledImageView.java
@@ -19,7 +19,6 @@
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
@@ -32,7 +31,6 @@
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.Px;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
@@ -47,8 +45,7 @@
*
* @hide
*/
-@TargetApi(Build.VERSION_CODES.M)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class CircledImageView extends View {
private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
@@ -133,13 +130,9 @@
if (mDrawable != null && mDrawable.getConstantState() != null) {
// The provided Drawable may be used elsewhere, so make a mutable clone before setTint()
// or setAlpha() is called on it.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- mDrawable =
- mDrawable.getConstantState()
- .newDrawable(context.getResources(), context.getTheme());
- } else {
- mDrawable = mDrawable.getConstantState().newDrawable(context.getResources());
- }
+ mDrawable =
+ mDrawable.getConstantState()
+ .newDrawable(context.getResources(), context.getTheme());
mDrawable = mDrawable.mutate();
}
diff --git a/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java b/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
index 275f1f8..5e88a8c 100644
--- a/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
+++ b/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
@@ -113,7 +113,7 @@
*/
public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) {
return;
- };
+ }
@VisibleForTesting
void setRound(boolean isScreenRound) {
diff --git a/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java b/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
index 08e8ec2..28e0570 100644
--- a/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
+++ b/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
@@ -19,14 +19,12 @@
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
import android.util.Property;
@@ -37,8 +35,7 @@
*
* @hide
*/
-@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class ProgressDrawable extends Drawable {
private static final Property<ProgressDrawable, Integer> LEVEL =
diff --git a/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java b/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
index fd09a87..300b6dd 100644
--- a/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
+++ b/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
@@ -15,7 +15,6 @@
*/
package android.support.wear.widget;
-import android.annotation.TargetApi;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
@@ -29,7 +28,6 @@
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -76,7 +74,6 @@
* app:radius="dimension"
* app:clipEnabled="boolean" /></pre>
*/
-@TargetApi(Build.VERSION_CODES.N)
public class RoundedDrawable extends Drawable {
@VisibleForTesting
diff --git a/wear/src/main/java/android/support/wear/widget/ScrollManager.java b/wear/src/main/java/android/support/wear/widget/ScrollManager.java
index 8155f62..e01a271 100644
--- a/wear/src/main/java/android/support/wear/widget/ScrollManager.java
+++ b/wear/src/main/java/android/support/wear/widget/ScrollManager.java
@@ -16,11 +16,8 @@
package android.support.wear.widget;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.annotation.TargetApi;
-import android.os.Build;
import android.support.annotation.RestrictTo;
+import android.support.annotation.RestrictTo.Scope;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.VelocityTracker;
@@ -30,8 +27,7 @@
*
* @hide
*/
-@TargetApi(Build.VERSION_CODES.M)
-@RestrictTo(LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class ScrollManager {
// One second in milliseconds.
private static final int ONE_SEC_IN_MS = 1000;
diff --git a/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java b/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
index a60b0bd..3a1e56b 100644
--- a/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
+++ b/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
@@ -29,7 +29,7 @@
* @hide Hidden until this goes through review
*/
@RequiresApi(Build.VERSION_CODES.KITKAT_WATCH)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class SimpleAnimatorListener implements Animator.AnimatorListener {
private boolean mWasCanceled;
diff --git a/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java b/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
index 6e7a6f3..33da79c 100644
--- a/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
@@ -16,12 +16,11 @@
package android.support.wear.widget;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import android.support.annotation.RestrictTo.Scope;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.util.Log;
@@ -40,7 +39,7 @@
*
* @hide
*/
-@RestrictTo(LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
@UiThread
class SwipeDismissLayout extends FrameLayout {
private static final String TAG = "SwipeDismissLayout";
diff --git a/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java b/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
index 5cacdfc..1425e68 100644
--- a/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
+++ b/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
@@ -16,11 +16,9 @@
package android.support.wear.widget;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Point;
-import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.wear.R;
@@ -35,7 +33,6 @@
*
* @see #setCircularScrollingGestureEnabled(boolean)
*/
-@TargetApi(Build.VERSION_CODES.M)
public class WearableRecyclerView extends RecyclerView {
private static final String TAG = "WearableRecyclerView";
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
index f1cb640..e9b2a40 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
@@ -32,7 +32,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class AbsListViewFlingWatcher implements FlingWatcher, OnScrollListener {
private final FlingListener mListener;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java b/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
index 3fe84c6..2fdfa13 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
@@ -33,7 +33,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class FlingWatcherFactory {
/**
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
index ca95ab2..4c0e5c8 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
@@ -38,7 +38,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class NestedScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener {
static final int MAX_WAIT_TIME_MS = 100;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java b/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
index 99c7c09..1285f72 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
@@ -54,7 +54,7 @@
* @hide
*/
@RequiresApi(Build.VERSION_CODES.M)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
public class PageIndicatorView extends View implements OnPageChangeListener {
private static final String TAG = "Dots";
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
index 7570fae..7916875 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
@@ -31,7 +31,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class RecyclerViewFlingWatcher extends OnScrollListener implements FlingWatcher {
private final FlingListener mListener;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
index f0b973b..5154e7b 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
@@ -38,7 +38,7 @@
*
* @hide
*/
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
class ScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener {
static final int MAX_WAIT_TIME_MS = 100;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
index 158467d..092ac72 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
@@ -16,12 +16,10 @@
package android.support.wear.widget.drawer;
-import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.Nullable;
import android.view.ActionProvider;
import android.view.ContextMenu;
@@ -34,7 +32,6 @@
import java.util.ArrayList;
import java.util.List;
-@TargetApi(Build.VERSION_CODES.M)
/* package */ class WearableActionDrawerMenu implements Menu {
private final Context mContext;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
index 03f494a..99cd4ff 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
@@ -16,12 +16,10 @@
package android.support.wear.widget.drawer;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -76,7 +74,6 @@
* <p>For {@link MenuItem}, setting and getting the title and icon, {@link MenuItem#getItemId}, and
* {@link MenuItem#setOnMenuItemClickListener} are implemented.
*/
-@TargetApi(Build.VERSION_CODES.M)
public class WearableActionDrawerView extends WearableDrawerView {
private static final String TAG = "WearableActionDrawer";
@@ -141,12 +138,8 @@
View peekView = layoutInflater.inflate(R.layout.ws_action_drawer_peek_view,
getPeekContainer(), false /* attachToRoot */);
setPeekContent(peekView);
- mPeekActionIcon =
- (ImageView) peekView
- .findViewById(R.id.ws_action_drawer_peek_action_icon);
- mPeekExpandIcon =
- (ImageView) peekView
- .findViewById(R.id.ws_action_drawer_expand_icon);
+ mPeekActionIcon = peekView.findViewById(R.id.ws_action_drawer_peek_action_icon);
+ mPeekExpandIcon = peekView.findViewById(R.id.ws_action_drawer_expand_icon);
} else {
mPeekActionIcon = null;
mPeekExpandIcon = null;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
index 6d27064..e100a46 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
@@ -19,11 +19,10 @@
import static android.support.wear.widget.drawer.WearableDrawerView.STATE_IDLE;
import static android.support.wear.widget.drawer.WearableDrawerView.STATE_SETTLING;
-import android.annotation.TargetApi;
import android.content.Context;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.NestedScrollingParent;
@@ -98,7 +97,6 @@
* </android.support.wear.widget.drawer.WearableDrawerView>
* </android.support.wear.widget.drawer.WearableDrawerLayout></pre>
*/
-@TargetApi(Build.VERSION_CODES.M)
public class WearableDrawerLayout extends FrameLayout
implements View.OnLayoutChangeListener, NestedScrollingParent, FlingListener {
@@ -654,12 +652,13 @@
}
@Override // NestedScrollingParent
- public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
+ boolean consumed) {
return false;
}
@Override // NestedScrollingParent
- public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
maybeUpdateScrollingContentView(target);
mLastScrollWasFling = true;
@@ -674,13 +673,13 @@
}
@Override // NestedScrollingParent
- public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
maybeUpdateScrollingContentView(target);
}
@Override // NestedScrollingParent
- public void onNestedScroll(
- View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
+ public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
boolean scrolledUp = dyConsumed < 0;
boolean scrolledDown = dyConsumed > 0;
@@ -873,18 +872,20 @@
}
@Override // NestedScrollingParent
- public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
+ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
+ int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override // NestedScrollingParent
- public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
+ int nestedScrollAxes) {
mCurrentNestedScrollSlopTracker = 0;
return true;
}
@Override // NestedScrollingParent
- public void onStopNestedScroll(View target) {
+ public void onStopNestedScroll(@NonNull View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
}
@@ -961,7 +962,7 @@
public abstract WearableDrawerView getDrawerView();
@Override
- public boolean tryCaptureView(View child, int pointerId) {
+ public boolean tryCaptureView(@NonNull View child, int pointerId) {
WearableDrawerView drawerView = getDrawerView();
// Returns true if the dragger is dragging the drawer.
return child == drawerView && !drawerView.isLocked()
@@ -969,13 +970,13 @@
}
@Override
- public int getViewVerticalDragRange(View child) {
+ public int getViewVerticalDragRange(@NonNull View child) {
// Defines the vertical drag range of the drawer.
return child == getDrawerView() ? child.getHeight() : 0;
}
@Override
- public void onViewCaptured(View capturedChild, int activePointerId) {
+ public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
showDrawerContentMaybeAnimate((WearableDrawerView) capturedChild);
}
@@ -1036,7 +1037,7 @@
private class TopDrawerDraggerCallback extends DrawerDraggerCallback {
@Override
- public int clampViewPositionVertical(View child, int top, int dy) {
+ public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if (mTopDrawerView == child) {
int peekHeight = mTopDrawerView.getPeekContainer().getHeight();
// The top drawer can be dragged vertically from peekHeight - height to 0.
@@ -1063,7 +1064,7 @@
}
@Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (releasedChild == mTopDrawerView) {
// Settle to final position. Either swipe open or close.
final float openedPercent = mTopDrawerView.getOpenedPercent();
@@ -1085,7 +1086,8 @@
}
@Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
+ int dy) {
if (changedView == mTopDrawerView) {
// Compute the offset and invalidate will move the drawer during layout.
final int height = changedView.getHeight();
@@ -1106,7 +1108,7 @@
private class BottomDrawerDraggerCallback extends DrawerDraggerCallback {
@Override
- public int clampViewPositionVertical(View child, int top, int dy) {
+ public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if (mBottomDrawerView == child) {
// The bottom drawer can be dragged vertically from (parentHeight - height) to
// (parentHeight - peekHeight).
@@ -1131,7 +1133,7 @@
}
@Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (releasedChild == mBottomDrawerView) {
// Settle to final position. Either swipe open or close.
final int parentHeight = getHeight();
@@ -1151,7 +1153,8 @@
}
@Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
+ int dy) {
if (changedView == mBottomDrawerView) {
// Compute the offset and invalidate will move the drawer during layout.
final int height = changedView.getHeight();
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
index dafac39..2462cba 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
@@ -16,11 +16,9 @@
package android.support.wear.widget.drawer;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.IdRes;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
@@ -87,7 +85,6 @@
* </LinearLayout>
* </android.support.wear.widget.drawer.WearableDrawerView></pre>
*/
-@TargetApi(Build.VERSION_CODES.M)
public class WearableDrawerView extends FrameLayout {
/**
* Indicates that the drawer is in an idle, settled state. No animation is in progress.
@@ -109,7 +106,7 @@
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY_GROUP)
+ @RestrictTo(Scope.LIBRARY)
@IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING})
public @interface DrawerState {}
@@ -155,8 +152,8 @@
setElevation(context.getResources()
.getDimension(R.dimen.ws_wearable_drawer_view_elevation));
- mPeekContainer = (ViewGroup) findViewById(R.id.ws_drawer_view_peek_container);
- mPeekIcon = (ImageView) findViewById(R.id.ws_drawer_view_peek_icon);
+ mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container);
+ mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon);
mPeekContainer.setOnClickListener(
new OnClickListener() {
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
index 480812b..c5c49fe 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
@@ -16,11 +16,9 @@
package android.support.wear.widget.drawer;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
@@ -58,7 +56,6 @@
* <p>The developer may specify which style to use with the {@code app:navigationStyle} custom
* attribute. If not specified, {@link #SINGLE_PAGE singlePage} will be used as the default.
*/
-@TargetApi(Build.VERSION_CODES.M)
public class WearableNavigationDrawerView extends WearableDrawerView {
private static final String TAG = "WearableNavDrawer";
@@ -79,7 +76,7 @@
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @RestrictTo(Scope.LIBRARY_GROUP)
+ @RestrictTo(Scope.LIBRARY)
@IntDef({SINGLE_PAGE, MULTI_PAGE})
public @interface NavigationStyle {}
@@ -282,7 +279,7 @@
/**
* @hide
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
+ @RestrictTo(Scope.LIBRARY)
public void setPresenter(WearableNavigationDrawerPresenter presenter) {
mPresenter = presenter;
}
diff --git a/wear/tests/AndroidManifest.xml b/wear/tests/AndroidManifest.xml
index ce78477..67ccd91 100644
--- a/wear/tests/AndroidManifest.xml
+++ b/wear/tests/AndroidManifest.xml
@@ -55,6 +55,13 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+
+ <activity android:name="android.support.wear.ambient.AmbientModeResumeTestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<!-- Test app is iOS compatible. -->
<meta-data
android:name="com.google.android.wearable.standalone"
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java
new file mode 100644
index 0000000..32de769
--- /dev/null
+++ b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.ambient;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.widget.util.WakeLockRule;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientModeResumeTest {
+ @Rule
+ public final WakeLockRule mWakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<AmbientModeResumeTestActivity> mActivityRule =
+ new ActivityTestRule<>(AmbientModeResumeTestActivity.class);
+
+ @Test
+ public void testActivityDefaults() throws Throwable {
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+ assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
+ }
+}
diff --git a/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java
new file mode 100644
index 0000000..0ca3c15
--- /dev/null
+++ b/wear/tests/src/android/support/wear/ambient/AmbientModeResumeTestActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.ambient;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class AmbientModeResumeTestActivity extends Activity {
+ AmbientMode.AmbientController mAmbientController;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAmbientController = AmbientMode.attachAmbientSupport(this);
+ }
+}
diff --git a/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java b/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
index 7823f23..0a76af0 100644
--- a/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
+++ b/wear/tests/src/com/google/android/wearable/compat/WearableActivityController.java
@@ -33,7 +33,7 @@
private AmbientCallback mCallback;
private boolean mAmbientEnabled = false;
- private boolean mAutoResumeEnabled = false;
+ private boolean mAutoResumeEnabled = true;
private boolean mAmbient = false;
public WearableActivityController(String tag, Activity activity, AmbientCallback callback) {
diff --git a/webkit/.gitignore b/webkit/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/webkit/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/webkit/AndroidManifest.xml b/webkit/AndroidManifest.xml
new file mode 100644
index 0000000..7d2bbc2
--- /dev/null
+++ b/webkit/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.webkit">
+</manifest>
+
diff --git a/webkit/OWNERS b/webkit/OWNERS
new file mode 100644
index 0000000..5d88928
--- /dev/null
+++ b/webkit/OWNERS
@@ -0,0 +1,5 @@
+boliu@google.com
+michaelbai@google.com
+tobiasjs@google.com
+torne@google.com
+gsennton@google.com
diff --git a/webkit/build.gradle b/webkit/build.gradle
new file mode 100644
index 0000000..e4fff11
--- /dev/null
+++ b/webkit/build.gradle
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+apply plugin: android.support.SupportAndroidLibraryPlugin
+
+android {
+ defaultConfig {
+ minSdkVersion 21
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ }
+}
+
+supportLibrary {
+ name = "WebView Support Library"
+ inceptionYear = "2017"
+ description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
+}
diff --git a/media-compat-test-client/lint-baseline.xml b/webkit/lint-baseline.xml
similarity index 100%
rename from media-compat-test-client/lint-baseline.xml
rename to webkit/lint-baseline.xml
diff --git a/media-compat-test-client/tests/NO_DOCS b/webkit/tests/NO_DOCS
similarity index 100%
rename from media-compat-test-client/tests/NO_DOCS
rename to webkit/tests/NO_DOCS