Merge "Further clarify docs for AmbientMode." into oc-mr1-jetpack-dev
diff --git a/annotations/build.gradle b/annotations/build.gradle
index af4174d..dd4a5bc 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -27,12 +27,6 @@
}
}
-// custom tasks for creating source/javadoc jars
-task sourcesJar(type: Jar, dependsOn:classes) {
- classifier = 'sources'
- from sourceSets.main.allSource
-}
-
task javadocJar(type: Jar, dependsOn:javadoc) {
classifier 'javadoc'
from javadoc.destinationDir
@@ -46,7 +40,6 @@
// add javadoc/source/annotations jar tasks as artifacts
artifacts {
archives jar
- archives sourcesJar
archives javadocJar
archives annotationsZip
}
diff --git a/app-toolkit/core-testing/build.gradle b/app-toolkit/core-testing/build.gradle
index 44d5ff0..127078d 100644
--- a/app-toolkit/core-testing/build.gradle
+++ b/app-toolkit/core-testing/build.gradle
@@ -39,9 +39,7 @@
dependencies {
compile project(":arch:runtime")
compile libs.support.annotations
- compile(libs.junit) {
- exclude module: 'hamcrest-core'
- }
+ compile(libs.junit)
compile libs.mockito_core, { exclude group: 'net.bytebuddy' }
testCompile libs.junit
diff --git a/app-toolkit/gradle/wrapper/gradle-wrapper.properties b/app-toolkit/gradle/wrapper/gradle-wrapper.properties
index d90766a..9d09896 100644
--- a/app-toolkit/gradle/wrapper/gradle-wrapper.properties
+++ b/app-toolkit/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=../../../../../tools/external/gradle/gradle-4.1-bin.zip
+distributionUrl=../../../../../tools/external/gradle/gradle-4.3-bin.zip
diff --git a/app-toolkit/init.gradle b/app-toolkit/init.gradle
index cd56153..d317d33 100644
--- a/app-toolkit/init.gradle
+++ b/app-toolkit/init.gradle
@@ -147,35 +147,6 @@
}
}
- project.plugins.whenPluginAdded { plugin ->
- def isAndroidLibrary = "com.android.build.gradle.LibraryPlugin"
- .equals(plugin.class.name)
- def isJavaLibrary = "org.gradle.api.plugins.JavaPlugin".equals(plugin.class.name)
- if (isAndroidLibrary) {
- // it is an android lib, enable sources.
- def sourcesTask = project.tasks.create(name: "sourcesJar", type : Jar) {
- classifier = 'sources'
- from android.sourceSets.main.getJava().getSrcDirs()
- }
- project.artifacts {
- archives sourcesTask
- }
- project.android.defaultConfig {
- // Update the version meta-data in each Manifest.
- addManifestPlaceholders(["version" : project.version])
- }
- } else if(isJavaLibrary && project.name == "common") {
- // it is a shared lib, enable sources.
- def sourcesTask = project.tasks.create(name: "sourcesJar", type : Jar) {
- classifier = 'sources'
- from sourceSets.main.allSource
- }
- project.artifacts {
- archives sourcesTask
- }
- }
- }
-
project.tasks.whenTaskAdded { task ->
if (task.name.startsWith("assembleAndroidTest")) {
buildServerAnchorTask.dependsOn task
diff --git a/buildSrc/src/main/groovy/android/support/FlatfootAndroidLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/FlatfootAndroidLibraryPlugin.groovy
index c6abac2..47a72d7 100644
--- a/buildSrc/src/main/groovy/android/support/FlatfootAndroidLibraryPlugin.groovy
+++ b/buildSrc/src/main/groovy/android/support/FlatfootAndroidLibraryPlugin.groovy
@@ -16,6 +16,7 @@
package android.support
+import com.android.build.gradle.LibraryExtension
import com.google.common.collect.ImmutableMap
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -33,5 +34,8 @@
VersionFileWriterTask.setUpAndroidLibrary(project);
project.apply(ImmutableMap.of("plugin", "com.android.library"));
+
+ LibraryExtension library = project.extensions.findByType(LibraryExtension.class);
+ SourceJarTaskHelper.setUpAndroidProject(project, library);
}
}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy
index 2170a50..86469ee 100644
--- a/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy
+++ b/buildSrc/src/main/groovy/android/support/SupportAndroidLibraryPlugin.groovy
@@ -135,21 +135,7 @@
project.uploadArchives.dependsOn "lintRelease"
}
- // Create sources jar for release builds
- library.getLibraryVariants().all(new Action<LibraryVariant>() {
- @Override
- public void execute(LibraryVariant libraryVariant) {
- if (!libraryVariant.getBuildType().getName().equals(BuilderConstants.RELEASE)) {
- return; // Skip non-release builds.
- }
-
- Jar sourceJar = project.getTasks().create("sourceJarRelease", Jar.class);
- sourceJar.preserveFileTimestamps = false;
- sourceJar.setClassifier("sources");
- sourceJar.from(library.getSourceSets().findByName("main").getJava().getSrcDirs());
- project.getArtifacts().add("archives", sourceJar);
- }
- });
+ SourceJarTaskHelper.setUpAndroidProject(project, library);
final ErrorProneToolChain toolChain = ErrorProneToolChain.create(project);
library.getBuildTypes().create("errorProne")
diff --git a/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy b/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy
index ae0d55c..3113e6c 100644
--- a/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy
+++ b/buildSrc/src/main/groovy/android/support/SupportJavaLibraryPlugin.groovy
@@ -20,6 +20,7 @@
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginConvention
/**
* Support java library specific plugin that sets common configurations needed for
@@ -41,5 +42,7 @@
targetCompatibility = version
}
}
+
+ SourceJarTaskHelper.setUpJavaProject(project);
}
}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/android/support/SourceJarTaskHelper.java b/buildSrc/src/main/java/android/support/SourceJarTaskHelper.java
new file mode 100644
index 0000000..9fbd1db
--- /dev/null
+++ b/buildSrc/src/main/java/android/support/SourceJarTaskHelper.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 android.support;
+
+import com.android.build.gradle.LibraryExtension;
+import com.android.builder.core.BuilderConstants;
+
+import org.gradle.api.Project;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.tasks.bundling.Jar;
+
+/**
+ * Helper class to handle creation of source jars.
+ */
+public class SourceJarTaskHelper {
+ /**
+ * Sets up a source jar task for an Android library project.
+ */
+ public static void setUpAndroidProject(Project project, LibraryExtension extension) {
+ // Create sources jar for release builds
+ extension.getLibraryVariants().all(libraryVariant -> {
+ if (!libraryVariant.getBuildType().getName().equals(BuilderConstants.RELEASE)) {
+ return; // Skip non-release builds.
+ }
+
+ Jar sourceJar = project.getTasks().create("sourceJarRelease", Jar.class);
+ sourceJar.setPreserveFileTimestamps(false);
+ sourceJar.setClassifier("sources");
+ sourceJar.from(extension.getSourceSets().findByName("main").getJava().getSrcDirs());
+ project.getArtifacts().add("archives", sourceJar);
+ });
+ }
+
+ /**
+ * Sets up a source jar task for a Java library project.
+ */
+ public static void setUpJavaProject(Project project) {
+ Jar sourceJar = project.getTasks().create("sourceJar", Jar.class);
+ sourceJar.setPreserveFileTimestamps(false);
+ sourceJar.setClassifier("sources");
+ JavaPluginConvention convention =
+ project.getConvention().getPlugin(JavaPluginConvention.class);
+ sourceJar.from(convention.getSourceSets().findByName("main").getAllSource().getSrcDirs());
+ project.getArtifacts().add("archives", sourceJar);
+ }
+}
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/values/attrs.xml b/car/res/values/attrs.xml
index 17a1960..0ba8f55 100644
--- a/car/res/values/attrs.xml
+++ b/car/res/values/attrs.xml
@@ -45,6 +45,12 @@
<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
diff --git a/car/res/values/colors.xml b/car/res/values/colors.xml
index 2099ae3..11880c3 100644
--- a/car/res/values/colors.xml
+++ b/car/res/values/colors.xml
@@ -19,14 +19,43 @@
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>
-
- <!-- Car specific colors that are not from the color palette. -->
- <color name="car_grey_650">#ff6B6B6B</color>
- <color name="car_darkbluegrey_700">#ff172026</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. -->
@@ -88,7 +117,7 @@
<!-- A light and dark colored card. -->
<color name="car_card_light">@color/car_grey_50</color>
- <color name="car_card_dark">@color/car_darkbluegrey_700</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>
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerController.java b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
index 4d9f4e9..823abd8 100644
--- a/car/src/main/java/android/support/car/drawer/CarDrawerController.java
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
@@ -19,16 +19,19 @@
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;
@@ -39,13 +42,21 @@
* 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 poopped from this list.
+ * the adapters are popped from this list.
*/
private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>();
@@ -78,16 +89,14 @@
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);
- mDrawerToggle = drawerToggle;
setupDrawerToggling();
}
@@ -124,6 +133,7 @@
mAdapterStack.peek().setTitleChangeListener(null);
mAdapterStack.push(adapter);
switchToAdapterInternal(adapter);
+ runLayoutAnimation(DRILL_DOWN_ANIM);
}
/** Close the drawer. */
@@ -272,7 +282,6 @@
// 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);
- scrollToPosition(0);
}
/**
@@ -291,6 +300,7 @@
adapter.setTitleChangeListener(null);
adapter.cleanup();
switchToAdapterInternal(mAdapterStack.peek());
+ runLayoutAnimation(NAVIGATE_UP_ANIM);
return true;
}
@@ -302,5 +312,17 @@
adapter.cleanup();
}
switchToAdapterInternal(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/widget/PagedListView.java b/car/src/main/java/android/support/car/widget/PagedListView.java
index 4652700..97a7041 100644
--- a/car/src/main/java/android/support/car/widget/PagedListView.java
+++ b/car/src/main/java/android/support/car/widget/PagedListView.java
@@ -23,6 +23,8 @@
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.support.annotation.IdRes;
import android.support.annotation.NonNull;
@@ -180,6 +182,11 @@
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
@@ -212,6 +219,16 @@
}
});
+ 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.
@@ -312,6 +329,16 @@
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.
*
@@ -422,6 +449,32 @@
}
/**
+ * 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.
*
@@ -766,16 +819,50 @@
}
/**
+ * 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.
*/
- public static class DividerDecoration extends RecyclerView.ItemDecoration {
+ 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 mDvidierEndId;
+ @IdRes private final int mDividerEndId;
/**
* @param dividerStartMargin The start offset of the dividing line. This offset will be
@@ -792,7 +879,7 @@
mContext = context;
mDividerStartMargin = dividerStartMargin;
mDividerStartId = dividerStartId;
- mDvidierEndId = dividerEndId;
+ mDividerEndId = dividerEndId;
Resources res = context.getResources();
mPaint = new Paint();
@@ -807,16 +894,20 @@
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
- for (int i = 0, childCount = parent.getChildCount(); i < childCount; i++) {
+ // 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 =
- mDvidierEndId != INVALID_RESOURCE_ID
- ? container.findViewById(mDvidierEndId)
+ mDividerEndId != INVALID_RESOURCE_ID
+ ? container.findViewById(mDividerEndId)
: container;
if (startChild == null || endChild == null) {
@@ -825,14 +916,24 @@
int left = mDividerStartMargin + startChild.getLeft();
int right = endChild.getRight();
- int bottom = container.getBottom();
+ int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2;
int top = bottom - mDividerHeight;
- // Draw a divider line between each item. No need to draw the line for the last
- // item.
- if (i != childCount - 1) {
- c.drawRect(left, top, right, bottom, mPaint);
- }
+ 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
index 125b354..be1d18b 100644
--- a/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
+++ b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
@@ -18,6 +18,7 @@
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;
@@ -98,6 +99,16 @@
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.
*
diff --git a/car/tests/AndroidManifest.xml b/car/tests/AndroidManifest.xml
index abd5264..f58390f 100644
--- a/car/tests/AndroidManifest.xml
+++ b/car/tests/AndroidManifest.xml
@@ -22,4 +22,4 @@
<activity android:name="android.support.car.widget.ColumnCardViewTestActivity"/>
<activity android:name="android.support.car.widget.PagedListViewTestActivity"/>
</application>
-</manifest>
\ No newline at end of file
+</manifest>
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_paged_list_view.xml b/car/tests/res/layout/activity_paged_list_view.xml
index ca8ba38..d14eb96 100644
--- a/car/tests/res/layout/activity_paged_list_view.xml
+++ b/car/tests/res/layout/activity_paged_list_view.xml
@@ -26,7 +26,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:showPagedListViewDivider="false"
- app:offsetScrollBar="true"
- />
-
-</FrameLayout>
\ No newline at end of file
+ app:offsetScrollBar="true"/>
+</FrameLayout>
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTest.java b/car/tests/src/android/support/car/widget/PagedListViewTest.java
index 4048434..bd673ad 100644
--- a/car/tests/src/android/support/car/widget/PagedListViewTest.java
+++ b/car/tests/src/android/support/car/widget/PagedListViewTest.java
@@ -35,15 +35,20 @@
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.IdlingRegistry;
import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.SmallTest;
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;
@@ -72,11 +77,13 @@
public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
new ActivityTestRule<>(PagedListViewTestActivity.class);
+ private PagedListViewTestActivity mActivity;
private PagedListView mPagedListView;
@Before
public void setUp() {
- mPagedListView = mActivityRule.getActivity().findViewById(R.id.paged_list_view);
+ mActivity = mActivityRule.getActivity();
+ mPagedListView = mActivity.findViewById(R.id.paged_list_view);
}
@After
@@ -253,6 +260,73 @@
assertThat(mPagedListView.getFirstFullyVisibleChildPosition(), is(equalTo(topPosition)));
}
+ @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;
}
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/emoji/appcompat/src/main/java/android/support/text/emoji/widget/EmojiAppCompatEditText.java b/emoji/appcompat/src/main/java/android/support/text/emoji/widget/EmojiAppCompatEditText.java
index 87c17c2..0ae4ea0 100644
--- a/emoji/appcompat/src/main/java/android/support/text/emoji/widget/EmojiAppCompatEditText.java
+++ b/emoji/appcompat/src/main/java/android/support/text/emoji/widget/EmojiAppCompatEditText.java
@@ -21,6 +21,7 @@
import android.support.annotation.Nullable;
import android.support.text.emoji.EmojiCompat;
import android.support.v7.widget.AppCompatEditText;
+import android.text.method.KeyListener;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -67,8 +68,11 @@
}
@Override
- public void setKeyListener(android.text.method.KeyListener input) {
- super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input));
+ public void setKeyListener(@Nullable KeyListener keyListener) {
+ if (keyListener != null) {
+ keyListener = getEmojiEditTextHelper().getKeyListener(keyListener);
+ }
+ super.setKeyListener(keyListener);
}
@Override
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
index a0e8a69..70ca7a6 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiEditText.java
@@ -21,6 +21,7 @@
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.support.text.emoji.EmojiCompat;
+import android.text.method.KeyListener;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -73,8 +74,11 @@
}
@Override
- public void setKeyListener(android.text.method.KeyListener keyListener) {
- super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener));
+ public void setKeyListener(@Nullable KeyListener keyListener) {
+ if (keyListener != null) {
+ keyListener = getEmojiEditTextHelper().getKeyListener(keyListener);
+ }
+ super.setKeyListener(keyListener);
}
@Override
diff --git a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiExtractEditText.java b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiExtractEditText.java
index ca1868e..2e4d3ca 100644
--- a/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiExtractEditText.java
+++ b/emoji/core/src/main/java/android/support/text/emoji/widget/EmojiExtractEditText.java
@@ -27,6 +27,7 @@
import android.support.annotation.RestrictTo;
import android.support.text.emoji.EmojiCompat;
import android.support.text.emoji.EmojiSpan;
+import android.text.method.KeyListener;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -81,8 +82,11 @@
}
@Override
- public void setKeyListener(android.text.method.KeyListener keyListener) {
- super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener));
+ public void setKeyListener(@Nullable KeyListener keyListener) {
+ if (keyListener != null) {
+ keyListener = getEmojiEditTextHelper().getKeyListener(keyListener);
+ }
+ super.setKeyListener(keyListener);
}
@Override
diff --git a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextTest.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextTest.java
index e4b452c..3528095 100644
--- a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextTest.java
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextTest.java
@@ -21,11 +21,13 @@
import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import android.annotation.TargetApi;
import android.app.Instrumentation;
import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
@@ -82,6 +84,15 @@
}
@Test
+ @UiThreadTest
+ public void testSetKeyListener_withNull() {
+ final TestActivity activity = mActivityRule.getActivity();
+ final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
+ editText.setKeyListener(null);
+ assertNull(editText.getKeyListener());
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 19)
@TargetApi(19)
public void testSetMaxCount() {
diff --git a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiExtractTextLayoutTest.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiExtractTextLayoutTest.java
index 8e1c6cf..6ed9b8e 100644
--- a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiExtractTextLayoutTest.java
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiExtractTextLayoutTest.java
@@ -18,6 +18,7 @@
import static android.support.text.emoji.util.EmojiMatcher.sameCharSequence;
+import static junit.framework.Assert.assertNull;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertEquals;
@@ -88,6 +89,21 @@
@Test
@UiThreadTest
+ public void testSetKeyListener_withNull() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+ .inflate(android.support.text.emoji.test.R.layout.extract_view, null);
+
+ final EmojiExtractEditText extractEditText = layout.findViewById(
+ android.R.id.inputExtractEditText);
+ assertNotNull(extractEditText);
+
+ extractEditText.setKeyListener(null);
+ assertNull(extractEditText.getKeyListener());
+ }
+
+ @Test
+ @UiThreadTest
public void testSetEmojiReplaceStrategy() {
final Context context = InstrumentationRegistry.getTargetContext();
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 6314766..96e6dab 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-4.1-bin.zip
+distributionUrl=../../../../tools/external/gradle/gradle-4.3-bin.zip
diff --git a/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java b/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
index 647d5d7..c1dc54d 100644
--- a/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
+++ b/lifecycle/extensions/src/test/java/android/arch/lifecycle/LiveDataTest.java
@@ -45,6 +45,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
@@ -52,11 +53,22 @@
@RunWith(JUnit4.class)
public class LiveDataTest {
private PublicLiveData<String> mLiveData;
- private LifecycleOwner mOwner;
- private LifecycleOwner mOwner2;
- private LifecycleRegistry mRegistry;
- private LifecycleRegistry mRegistry2;
private MethodExec mActiveObserversChanged;
+
+ private LifecycleOwner mOwner;
+ private LifecycleRegistry mRegistry;
+
+ private LifecycleOwner mOwner2;
+ private LifecycleRegistry mRegistry2;
+
+ private LifecycleOwner mOwner3;
+ private Lifecycle mLifecycle3;
+ private Observer<String> mObserver3;
+
+ private LifecycleOwner mOwner4;
+ private Lifecycle mLifecycle4;
+ private Observer<String> mObserver4;
+
private boolean mInObserver;
@Before
@@ -67,12 +79,10 @@
mLiveData.activeObserversChanged = mActiveObserversChanged;
mOwner = mock(LifecycleOwner.class);
-
mRegistry = new LifecycleRegistry(mOwner);
when(mOwner.getLifecycle()).thenReturn(mRegistry);
mOwner2 = mock(LifecycleOwner.class);
-
mRegistry2 = new LifecycleRegistry(mOwner2);
when(mOwner2.getLifecycle()).thenReturn(mRegistry2);
@@ -80,6 +90,19 @@
}
@Before
+ public void initNonLifecycleRegistry() {
+ mOwner3 = mock(LifecycleOwner.class);
+ mLifecycle3 = mock(Lifecycle.class);
+ mObserver3 = (Observer<String>) mock(Observer.class);
+ when(mOwner3.getLifecycle()).thenReturn(mLifecycle3);
+
+ mOwner4 = mock(LifecycleOwner.class);
+ mLifecycle4 = mock(Lifecycle.class);
+ mObserver4 = (Observer<String>) mock(Observer.class);
+ when(mOwner4.getLifecycle()).thenReturn(mLifecycle4);
+ }
+
+ @Before
public void swapExecutorDelegate() {
ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
}
@@ -572,100 +595,195 @@
verify(mActiveObserversChanged, never()).onCall(anyBoolean());
}
- /**
- * Verifies that if a lifecycle's state changes without an event, and changes to something that
- * LiveData would become inactive in response to, LiveData will detect the change upon new data
- * being set and become inactive. Also verifies that once the lifecycle enters into a state
- * that LiveData should become active to, that it does indeed become active.
- */
@Test
- public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_oneObserver() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
+ public void setValue_lifecycleIsCreatedNoEvent_liveDataBecomesInactiveAndObserverNotCalled() {
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ // Arrange.
- // Marking state as CREATED should call onInactive.
+ mLiveData.observe(mOwner3, mObserver3);
+
+ GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
reset(mActiveObserversChanged);
- mRegistry.markState(Lifecycle.State.CREATED);
- verify(mActiveObserversChanged).onCall(false);
- reset(mActiveObserversChanged);
+ reset(mObserver3);
- // Setting a new value should trigger LiveData to realize the Lifecycle it is observing
- // is in a state where the LiveData should be inactive, so the LiveData will call onInactive
- // and the Observer shouldn't be affected.
+ // Act.
+
mLiveData.setValue("1");
- // state is already CREATED so should not call again
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- verify(observer, never()).onChanged(anyString());
+ // Assert.
- // Sanity check. Because we've only marked the state as CREATED, sending ON_START
- // should re-dispatch events.
- reset(mActiveObserversChanged);
- reset(observer);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- verify(mActiveObserversChanged).onCall(true);
- verify(observer).onChanged("1");
+ verify(mActiveObserversChanged).onCall(false);
+ verify(mObserver3, never()).onChanged(anyString());
}
- /**
- * This test verifies that LiveData will detect changes in LifecycleState that would make it
- * inactive upon the setting of new data, but only if all of the Lifecycles it's observing
- * are all in those states. It also makes sure that once it is inactive, that it will become
- * active again once one of the lifecycles it's observing moves to an appropriate state.
+ /*
+ * Arrange: LiveData was made inactive via SetValue (because the Lifecycle it was
+ * observing was in the CREATED state and no event was dispatched).
+ * Act: Lifecycle enters Started state and dispatches event.
+ * Assert: LiveData becomes active and dispatches new value to observer.
*/
@Test
- public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_twoObservers() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
+ public void test_liveDataInactiveViaSetValueThenLifecycleResumes() {
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
+ // Arrange.
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mLiveData.observe(mOwner3, mObserver3);
- // Marking the state to created won't change LiveData to be inactive.
- reset(mActiveObserversChanged);
- mRegistry.markState(Lifecycle.State.CREATED);
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
- // After setting a value, the LiveData will stay active because there is still a STARTED
- // lifecycle being observed. The one Observer associated with the STARTED lifecycle will
- // also have been called, but the other Observer will not have been called.
- reset(observer1);
- reset(observer2);
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
mLiveData.setValue("1");
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- verify(observer1, never()).onChanged(anyString());
- verify(observer2).onChanged("1");
- // Now we set the other Lifecycle to be inactive, live data should become inactive.
- reset(observer1);
- reset(observer2);
- mRegistry2.markState(Lifecycle.State.CREATED);
- verify(mActiveObserversChanged).onCall(false);
- verify(observer1, never()).onChanged(anyString());
- verify(observer2, never()).onChanged(anyString());
-
- // Now we post another value, because both lifecycles are in the Created state, live data
- // will not dispatch any values
reset(mActiveObserversChanged);
- mLiveData.setValue("2");
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- verify(observer1, never()).onChanged(anyString());
- verify(observer2, never()).onChanged(anyString());
+ reset(mObserver3);
- // Now that the first Lifecycle has been moved back to the Resumed state, the LiveData will
- // be made active and it's associated Observer will be called with the new value, but the
- // Observer associated with the Lifecycle that is still in the Created state won't be
- // called.
- reset(mActiveObserversChanged);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ // Act.
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ // Assert.
+
verify(mActiveObserversChanged).onCall(true);
- verify(observer1).onChanged("2");
- verify(observer2, never()).onChanged(anyString());
+ verify(mObserver3).onChanged("1");
+ }
+
+ /*
+ * Arrange: One of two Lifecycles enter the CREATED state without sending an event.
+ * Act: Lifecycle's setValue method is called with new value.
+ * Assert: LiveData stays active and new value is dispatched to Lifecycle that is still at least
+ * STARTED.
+ */
+ @Test
+ public void setValue_oneOfTwoLifecyclesAreCreatedNoEvent() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ mLiveData.setValue("1");
+
+ // Assert.
+
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ verify(mObserver3, never()).onChanged(anyString());
+ verify(mObserver4).onChanged("1");
+ }
+
+ /*
+ * Arrange: Two observed Lifecycles enter the CREATED state without sending an event.
+ * Act: Lifecycle's setValue method is called with new value.
+ * Assert: LiveData becomes inactive and nothing is dispatched to either observer.
+ */
+ @Test
+ public void setValue_twoLifecyclesAreCreatedNoEvent() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ mLiveData.setValue("1");
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(false);
+ verify(mObserver3, never()).onChanged(anyString());
+ verify(mObserver3, never()).onChanged(anyString());
+ }
+
+ /*
+ * Arrange: LiveData was made inactive via SetValue (because both Lifecycles it was
+ * observing were in the CREATED state and no event was dispatched).
+ * Act: One Lifecycle enters STARTED state and dispatches lifecycle event.
+ * Assert: LiveData becomes active and dispatches new value to observer associated with started
+ * Lifecycle.
+ */
+ @Test
+ public void test_liveDataInactiveViaSetValueThenOneLifecycleResumes() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ mLiveData.setValue("1");
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(true);
+ verify(mObserver3).onChanged("1");
+ verify(mObserver4, never()).onChanged(anyString());
+ }
+
+ private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
+ ArgumentCaptor<GenericLifecycleObserver> captor =
+ ArgumentCaptor.forClass(GenericLifecycleObserver.class);
+ verify(lifecycle).addObserver(captor.capture());
+ return (captor.getValue());
}
@SuppressWarnings("WeakerAccess")
diff --git a/lifecycle/gradle/wrapper/gradle-wrapper.properties b/lifecycle/gradle/wrapper/gradle-wrapper.properties
index 467c103..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.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java b/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java
index 2b25bc9..ba76f8e 100644
--- a/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java
+++ b/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java
@@ -24,7 +24,7 @@
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
-import java.lang.ref.WeakReference;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Adapts {@link LiveData} input and output to the ReactiveStreams spec.
@@ -53,83 +53,114 @@
public static <T> Publisher<T> toPublisher(
final LifecycleOwner lifecycle, final LiveData<T> liveData) {
- return new Publisher<T>() {
+ return new LiveDataPublisher<>(lifecycle, liveData);
+ }
+
+ private static final class LiveDataPublisher<T> implements Publisher<T> {
+ final LifecycleOwner mLifecycle;
+ final LiveData<T> mLiveData;
+
+ LiveDataPublisher(final LifecycleOwner lifecycle, final LiveData<T> liveData) {
+ this.mLifecycle = lifecycle;
+ this.mLiveData = liveData;
+ }
+
+ @Override
+ public void subscribe(Subscriber<? super T> subscriber) {
+ subscriber.onSubscribe(new LiveDataSubscription<T>(subscriber, mLifecycle, mLiveData));
+ }
+
+ static final class LiveDataSubscription<T> implements Subscription, Observer<T> {
+ final Subscriber<? super T> mSubscriber;
+ final LifecycleOwner mLifecycle;
+ final LiveData<T> mLiveData;
+
+ volatile boolean mCanceled;
+ // used on main thread only
boolean mObserving;
- boolean mCanceled;
long mRequested;
+ // used on main thread only
@Nullable
T mLatest;
+ LiveDataSubscription(final Subscriber<? super T> subscriber,
+ final LifecycleOwner lifecycle, final LiveData<T> liveData) {
+ this.mSubscriber = subscriber;
+ this.mLifecycle = lifecycle;
+ this.mLiveData = liveData;
+ }
+
@Override
- public void subscribe(final Subscriber<? super T> subscriber) {
- final Observer<T> observer = new Observer<T>() {
+ public void onChanged(T t) {
+ if (mCanceled) {
+ return;
+ }
+ if (mRequested > 0) {
+ mLatest = null;
+ mSubscriber.onNext(t);
+ if (mRequested != Long.MAX_VALUE) {
+ mRequested--;
+ }
+ } else {
+ mLatest = t;
+ }
+ }
+
+ @Override
+ public void request(final long n) {
+ if (mCanceled) {
+ return;
+ }
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
@Override
- public void onChanged(@Nullable T t) {
+ public void run() {
if (mCanceled) {
return;
}
- if (mRequested > 0) {
+ if (n <= 0L) {
+ mCanceled = true;
+ if (mObserving) {
+ mLiveData.removeObserver(LiveDataSubscription.this);
+ mObserving = false;
+ }
mLatest = null;
- subscriber.onNext(t);
- if (mRequested != Long.MAX_VALUE) {
- mRequested--;
- }
- } else {
- mLatest = t;
- }
- }
- };
-
- subscriber.onSubscribe(new Subscription() {
- @Override
- public void request(final long n) {
- if (n < 0 || mCanceled) {
+ mSubscriber.onError(
+ new IllegalArgumentException("Non-positive request"));
return;
}
- ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- if (mCanceled) {
- return;
- }
- // Prevent overflowage.
- mRequested = mRequested + n >= mRequested
- ? mRequested + n : Long.MAX_VALUE;
- if (!mObserving) {
- mObserving = true;
- liveData.observe(lifecycle, observer);
- } else if (mLatest != null) {
- observer.onChanged(mLatest);
- mLatest = null;
- }
- }
- });
- }
- @Override
- public void cancel() {
- if (mCanceled) {
- return;
+ // Prevent overflowage.
+ mRequested = mRequested + n >= mRequested
+ ? mRequested + n : Long.MAX_VALUE;
+ if (!mObserving) {
+ mObserving = true;
+ mLiveData.observe(mLifecycle, LiveDataSubscription.this);
+ } else if (mLatest != null) {
+ onChanged(mLatest);
+ mLatest = null;
}
- ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- if (mCanceled) {
- return;
- }
- if (mObserving) {
- liveData.removeObserver(observer);
- mObserving = false;
- }
- mLatest = null;
- mCanceled = true;
- }
- });
}
});
}
- };
+ @Override
+ public void cancel() {
+ if (mCanceled) {
+ return;
+ }
+ mCanceled = true;
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mObserving) {
+ mLiveData.removeObserver(LiveDataSubscription.this);
+ mObserving = false;
+ }
+ mLatest = null;
+ }
+ });
+ }
+ }
}
/**
@@ -145,6 +176,10 @@
* Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is
* added, it will automatically notify with the last value held in LiveData,
* which might not be the last value emitted by the Publisher.
+ * <p>
+ * Note that LiveData does NOT handle errors and it expects that errors are treated as states
+ * in the data that's held. In case of an error being emitted by the publisher, an error will
+ * be propagated to the main thread and the app will crash.
*
* @param <T> The type of data hold by this instance.
*/
@@ -166,67 +201,80 @@
* added, it will automatically notify with the last value held in LiveData,
* which might not be the last value emitted by the Publisher.
*
+ * <p>
+ * Note that LiveData does NOT handle errors and it expects that errors are treated as states
+ * in the data that's held. In case of an error being emitted by the publisher, an error will
+ * be propagated to the main thread and the app will crash.
+ *
* @param <T> The type of data hold by this instance.
*/
private static class PublisherLiveData<T> extends LiveData<T> {
- private WeakReference<Subscription> mSubscriptionRef;
private final Publisher mPublisher;
- private final Object mLock = new Object();
+ final AtomicReference<LiveDataSubscriber> mSubscriber;
PublisherLiveData(@NonNull final Publisher publisher) {
mPublisher = publisher;
+ mSubscriber = new AtomicReference<>();
}
@Override
protected void onActive() {
super.onActive();
-
- mPublisher.subscribe(new Subscriber<T>() {
- @Override
- public void onSubscribe(Subscription s) {
- // Don't worry about backpressure. If the stream is too noisy then
- // backpressure can be handled upstream.
- synchronized (mLock) {
- s.request(Long.MAX_VALUE);
- mSubscriptionRef = new WeakReference<>(s);
- }
- }
-
- @Override
- public void onNext(final T t) {
- postValue(t);
- }
-
- @Override
- public void onError(Throwable t) {
- synchronized (mLock) {
- mSubscriptionRef = null;
- }
- // Errors should be handled upstream, so propagate as a crash.
- throw new RuntimeException(t);
- }
-
- @Override
- public void onComplete() {
- synchronized (mLock) {
- mSubscriptionRef = null;
- }
- }
- });
-
+ LiveDataSubscriber s = new LiveDataSubscriber();
+ mSubscriber.set(s);
+ mPublisher.subscribe(s);
}
@Override
protected void onInactive() {
super.onInactive();
- synchronized (mLock) {
- WeakReference<Subscription> subscriptionRef = mSubscriptionRef;
- if (subscriptionRef != null) {
- Subscription subscription = subscriptionRef.get();
- if (subscription != null) {
- subscription.cancel();
+ LiveDataSubscriber s = mSubscriber.getAndSet(null);
+ if (s != null) {
+ s.cancelSubscription();
+ }
+ }
+
+ final class LiveDataSubscriber extends AtomicReference<Subscription>
+ implements Subscriber<T> {
+
+ @Override
+ public void onSubscribe(Subscription s) {
+ if (compareAndSet(null, s)) {
+ s.request(Long.MAX_VALUE);
+ } else {
+ s.cancel();
+ }
+ }
+
+ @Override
+ public void onNext(T item) {
+ postValue(item);
+ }
+
+ @Override
+ public void onError(final Throwable ex) {
+ mSubscriber.compareAndSet(this, null);
+
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ // Errors should be handled upstream, so propagate as a crash.
+ throw new RuntimeException("LiveData does not handle errors. Errors from "
+ + "publishers should be handled upstream and propagated as "
+ + "state", ex);
}
- mSubscriptionRef = null;
+ });
+ }
+
+ @Override
+ public void onComplete() {
+ mSubscriber.compareAndSet(this, null);
+ }
+
+ public void cancelSubscription() {
+ Subscription s = get();
+ if (s != null) {
+ s.cancel();
}
}
}
diff --git a/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
index 7278847..83e543c 100644
--- a/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
+++ b/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
@@ -16,6 +16,9 @@
package android.arch.lifecycle;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.fail;
+
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -34,6 +37,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import io.reactivex.Flowable;
import io.reactivex.disposables.Disposable;
@@ -115,6 +119,41 @@
}
@Test
+ public void convertsFromPublisherSubscribeWithDelay() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ processor.delaySubscription(100, TimeUnit.SECONDS, sBackgroundScheduler);
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ liveData.removeObserver(mObserver);
+ sBackgroundScheduler.triggerActions();
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("bar");
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "bar", "baz")));
+ }
+
+ @Test
+ public void convertsFromPublisherThrowsException() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ IllegalStateException exception = new IllegalStateException("test exception");
+ try {
+ processor.onError(exception);
+ fail("Runtime Exception expected");
+ } catch (RuntimeException ex) {
+ assertEquals(ex.getCause(), exception);
+ }
+ }
+
+ @Test
public void convertsFromPublisherWithMultipleObservers() {
final List<String> output2 = new ArrayList<>();
PublishProcessor<String> processor = PublishProcessor.create();
@@ -125,7 +164,7 @@
processor.onNext("foo");
processor.onNext("bar");
- // The second mObserver should only get the newest value and any later values.
+ // The second observer should only get the newest value and any later values.
liveData.observe(mLifecycleOwner, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
@@ -140,6 +179,32 @@
}
@Test
+ public void convertsFromPublisherWithMultipleObserversAfterInactive() {
+ final List<String> output2 = new ArrayList<>();
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ processor.onNext("bar");
+
+ // The second observer should only get the newest value and any later values.
+ liveData.observe(mLifecycleOwner, new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ output2.add(s);
+ }
+ });
+
+ liveData.removeObserver(mObserver);
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar")));
+ assertThat(output2, is(Arrays.asList("bar", "baz")));
+ }
+
+ @Test
public void convertsFromPublisherAfterInactive() {
PublishProcessor<String> processor = PublishProcessor.create();
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
@@ -156,7 +221,7 @@
}
@Test
- public void convertsFromPublisherManagesSubcriptions() {
+ public void convertsFromPublisherManagesSubscriptions() {
PublishProcessor<String> processor = PublishProcessor.create();
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
@@ -198,7 +263,7 @@
assertThat(
mOutputProcessor.getValues(new String[]{}),
- is(new String[] {"foo", "bar", "baz"}));
+ is(new String[]{"foo", "bar", "baz"}));
}
@Test
@@ -263,10 +328,10 @@
final Subscription subscription = subscriptionSubject.blockingSingle();
subscription.request(1);
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {}));
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
liveData.setValue("foo");
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"}));
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
subscription.request(2);
liveData.setValue("baz");
@@ -274,7 +339,7 @@
assertThat(
mOutputProcessor.getValues(new String[]{}),
- is(new String[] {"foo", "baz", "fizz"}));
+ is(new String[]{"foo", "baz", "fizz"}));
// 'nyan' will be dropped as there is nothing currently requesting a stream.
liveData.setValue("nyan");
@@ -282,13 +347,13 @@
assertThat(
mOutputProcessor.getValues(new String[]{}),
- is(new String[] {"foo", "baz", "fizz"}));
+ is(new String[]{"foo", "baz", "fizz"}));
// When a new request comes in, the latest value will be pushed.
subscription.request(1);
assertThat(
mOutputProcessor.getValues(new String[]{}),
- is(new String[] {"foo", "baz", "fizz", "cat"}));
+ is(new String[]{"foo", "baz", "fizz", "cat"}));
}
@Test
@@ -301,17 +366,17 @@
liveData.setValue("foo");
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {}));
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
sBackgroundScheduler.triggerActions();
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"}));
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
liveData.setValue("bar");
liveData.setValue("baz");
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"}));
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
sBackgroundScheduler.triggerActions();
assertThat(mOutputProcessor.getValues(
new String[]{}),
- is(new String[] {"foo", "bar", "baz"}));
+ is(new String[]{"foo", "bar", "baz"}));
}
}
diff --git a/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
index 23d6d1e..6bfd828 100644
--- a/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -47,6 +47,8 @@
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.test.InstrumentationRegistry.getArguments;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
@@ -69,6 +71,7 @@
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;
@@ -84,6 +87,8 @@
@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;
@@ -107,9 +112,8 @@
"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;
@@ -120,6 +124,10 @@
@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();
diff --git a/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
index 3b1e421..6f5e07e 100644
--- a/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
+++ b/media-compat/cross-app-tests/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -47,6 +47,8 @@
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.test.InstrumentationRegistry.getArguments;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.InstrumentationRegistry.getTargetContext;
@@ -80,6 +82,7 @@
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 junit.framework.Assert;
@@ -96,6 +99,9 @@
*/
@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;
@@ -107,6 +113,8 @@
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();
@@ -116,6 +124,10 @@
@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() {
diff --git a/media-compat/cross-app-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java b/media-compat/cross-app-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
new file mode 100644
index 0000000..6533ee1
--- /dev/null
+++ b/media-compat/cross-app-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
@@ -0,0 +1,25 @@
+/*
+ * 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";
+}
diff --git a/media-compat/cross-app-tests/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media-compat/cross-app-tests/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
index a30f82d..61f98f4 100644
--- a/media-compat/cross-app-tests/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
+++ b/media-compat/cross-app-tests/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -50,6 +50,8 @@
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.test.InstrumentationRegistry.getArguments;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.InstrumentationRegistry.getTargetContext;
@@ -69,6 +71,7 @@
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;
@@ -83,6 +86,9 @@
*/
@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;
@@ -90,11 +96,16 @@
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() {
diff --git a/room/gradle/wrapper/gradle-wrapper.properties b/room/gradle/wrapper/gradle-wrapper.properties
index a5292b0..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.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 2d79f3e..5423d04 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -164,8 +164,10 @@
* Set the presenter selector used to create and bind views.
*/
public final void setPresenterSelector(PresenterSelector presenterSelector) {
- mPresenterSelector = presenterSelector;
- updateAdapter();
+ if (mPresenterSelector != presenterSelector) {
+ mPresenterSelector = presenterSelector;
+ updateAdapter();
+ }
}
/**
@@ -180,8 +182,10 @@
* @param rowsAdapter Adapter that represents list of rows.
*/
public final void setAdapter(ObjectAdapter rowsAdapter) {
- mAdapter = rowsAdapter;
- updateAdapter();
+ if (mAdapter != rowsAdapter) {
+ mAdapter = rowsAdapter;
+ updateAdapter();
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
index dba78da..6a477ab 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -161,8 +161,10 @@
* Set the presenter selector used to create and bind views.
*/
public final void setPresenterSelector(PresenterSelector presenterSelector) {
- mPresenterSelector = presenterSelector;
- updateAdapter();
+ if (mPresenterSelector != presenterSelector) {
+ mPresenterSelector = presenterSelector;
+ updateAdapter();
+ }
}
/**
@@ -177,8 +179,10 @@
* @param rowsAdapter Adapter that represents list of rows.
*/
public final void setAdapter(ObjectAdapter rowsAdapter) {
- mAdapter = rowsAdapter;
- updateAdapter();
+ if (mAdapter != rowsAdapter) {
+ mAdapter = rowsAdapter;
+ updateAdapter();
+ }
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index ae31c4f..b358efc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -570,14 +570,27 @@
}
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) {
- swap = true;
+ 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;
}
@@ -590,25 +603,29 @@
"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;
- }
+ 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}
@@ -678,7 +695,8 @@
MainFragmentAdapter mMainFragmentAdapter;
Fragment mMainFragment;
HeadersFragment mHeadersFragment;
- private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ ListRowDataAdapter mMainFragmentListRowDataAdapter;
private ObjectAdapter mAdapter;
private PresenterSelector mAdapterPresenter;
@@ -701,6 +719,7 @@
private int mSelectedPosition = -1;
private float mScaleFactor;
boolean mIsPageRow;
+ Object mPageRow;
private PresenterSelector mHeaderPresenterSelector;
private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -820,11 +839,45 @@
return;
}
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(
- adapter == null ? null : new ListRowDataAdapter(adapter));
+ updateMainFragmentRowsAdapter();
+ mHeadersFragment.setAdapter(mAdapter);
+ }
+
+ void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+ if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+ return;
}
- mHeadersFragment.setAdapter(adapter);
+ // 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() {
@@ -1144,7 +1197,8 @@
@Override
public void onDestroyView() {
- mMainFragmentRowsAdapter = null;
+ setMainFragmentRowsAdapter(null);
+ mPageRow = null;
mMainFragmentAdapter = null;
mMainFragment = null;
mHeadersFragment = null;
@@ -1198,26 +1252,17 @@
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);
+ // 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;
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- } else {
- mMainFragmentRowsAdapter = null;
- }
+ setMainFragmentAdapter();
}
mHeadersFragment.setHeadersGone(!mCanShowHeaders);
@@ -1242,8 +1287,6 @@
mScaleFrameLayout.setPivotX(0);
mScaleFrameLayout.setPivotY(mContainerListAlignTop);
- setupMainFragment();
-
if (mBrandColorSet) {
mHeadersFragment.setBackgroundColor(mBrandColor);
}
@@ -1270,17 +1313,6 @@
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
@@ -1470,10 +1502,10 @@
};
void onRowSelected(int position) {
- if (position != mSelectedPosition) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
- }
+ // 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) {
@@ -1500,7 +1532,6 @@
if (createMainFragment(mAdapter, position)) {
swapToMainFragment();
expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
- setupMainFragment();
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
index 4a2502a..c28064c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -567,14 +567,27 @@
}
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) {
- swap = true;
+ 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;
}
@@ -587,25 +600,29 @@
"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;
- }
+ 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}
@@ -675,7 +692,8 @@
MainFragmentAdapter mMainFragmentAdapter;
Fragment mMainFragment;
HeadersSupportFragment mHeadersSupportFragment;
- private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+ ListRowDataAdapter mMainFragmentListRowDataAdapter;
private ObjectAdapter mAdapter;
private PresenterSelector mAdapterPresenter;
@@ -698,6 +716,7 @@
private int mSelectedPosition = -1;
private float mScaleFactor;
boolean mIsPageRow;
+ Object mPageRow;
private PresenterSelector mHeaderPresenterSelector;
private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -817,11 +836,45 @@
return;
}
- if (mMainFragmentRowsAdapter != null) {
- mMainFragmentRowsAdapter.setAdapter(
- adapter == null ? null : new ListRowDataAdapter(adapter));
+ updateMainFragmentRowsAdapter();
+ mHeadersSupportFragment.setAdapter(mAdapter);
+ }
+
+ void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+ if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+ return;
}
- mHeadersSupportFragment.setAdapter(adapter);
+ // 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() {
@@ -1141,7 +1194,8 @@
@Override
public void onDestroyView() {
- mMainFragmentRowsAdapter = null;
+ setMainFragmentRowsAdapter(null);
+ mPageRow = null;
mMainFragmentAdapter = null;
mMainFragment = null;
mHeadersSupportFragment = null;
@@ -1195,26 +1249,17 @@
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);
+ // 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;
- if (!mIsPageRow) {
- if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
- mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
- .getMainFragmentRowsAdapter();
- } else {
- mMainFragmentRowsAdapter = null;
- }
- } else {
- mMainFragmentRowsAdapter = null;
- }
+ setMainFragmentAdapter();
}
mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
@@ -1239,8 +1284,6 @@
mScaleFrameLayout.setPivotX(0);
mScaleFrameLayout.setPivotY(mContainerListAlignTop);
- setupMainFragment();
-
if (mBrandColorSet) {
mHeadersSupportFragment.setBackgroundColor(mBrandColor);
}
@@ -1267,17 +1310,6 @@
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
@@ -1467,10 +1499,10 @@
};
void onRowSelected(int position) {
- if (position != mSelectedPosition) {
- mSetSelectionRunnable.post(
- position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
- }
+ // 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) {
@@ -1497,7 +1529,6 @@
if (createMainFragment(mAdapter, position)) {
swapToMainFragment();
expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
- setupMainFragment();
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
index f9af12f..03d948b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
@@ -13,6 +13,7 @@
* 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;
@@ -22,6 +23,7 @@
private final ObjectAdapter mAdapter;
int mLastVisibleRowIndex;
+ final DataObserver mDataObserver;
public ListRowDataAdapter(ObjectAdapter adapter) {
super(adapter.getPresenterSelector());
@@ -34,10 +36,20 @@
// 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());
+ mDataObserver = new SimpleDataObserver();
} else {
- mAdapter.registerObserver(new QueueBasedDataObserver());
+ mDataObserver = new QueueBasedDataObserver();
}
+ attach();
+ }
+
+ void detach() {
+ mAdapter.unregisterObserver(mDataObserver);
+ }
+
+ void attach() {
+ initialize();
+ mAdapter.registerObserver(mDataObserver);
}
void initialize() {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
index 535f81b..d411f9e 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
@@ -13,7 +13,10 @@
*/
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
@@ -132,6 +135,10 @@
mObservers.get(i).onItemMoved(positionStart, toPosition);
}
}
+
+ boolean hasObserver() {
+ return mObservers.size() > 0;
+ }
}
private final DataObservable mObservable = new DataObservable();
@@ -207,6 +214,14 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final boolean hasObserver() {
+ return mObservable.hasObserver();
+ }
+
+ /**
* Unregisters all DataObservers for this ObjectAdapter.
*/
public final void unregisterAllObservers() {
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
index 08e38ca..b29153a 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -16,6 +16,8 @@
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;
@@ -86,6 +88,49 @@
}
}
+ 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) {
@@ -497,6 +542,48 @@
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;
}
/**
@@ -506,14 +593,7 @@
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++);
+ ObjectAdapter listRowAdapter = createListRowAdapter();
HeaderItem header = new HeaderItem(i, "Row " + i);
adapter.add(new ListRow(header, listRowAdapter));
}
@@ -548,19 +628,250 @@
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(createListRowsAdapter());
+ 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.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.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 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)
@@ -572,27 +883,15 @@
fragment.assertExecutedEntranceTransition();
// select item 2 on row 1
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fragment.setSelectedPosition(1, true,
- new ListRowPresenter.SelectItemViewHolderTask(2));
- }
- });
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
// save activity to state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
// recreate activity with saved state
SingleFragmentTestActivity activity2 = launchAndWaitActivity(
RowsFragmentTest.F_standard.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
// validate restored activity selected row and selected item
fragment2.assertNoEntranceTransition();
@@ -703,23 +1002,47 @@
}
/**
+ * 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 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;
- }
- });
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
}
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -735,19 +1058,28 @@
}
}, 100);
}
+ }
- /**
- * Util to wait PageFragment swapped.
- */
- Fragment waitPageFragment(final Class pageFragmentClass) {
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ /**
+ * 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 boolean canProceed() {
- return pageFragmentClass.isInstance(getMainFragment())
- && getMainFragment().getView() != null;
+ public void run() {
+ setAdapter(create2PageRow3ListRow());
+ startEntranceTransition();
}
- });
- return getMainFragment();
+ }, 100);
}
}
@@ -760,27 +1092,14 @@
fragment.assertExecutedEntranceTransition();
// select item 2 on row 1.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fragment.setSelectedPosition(1, true,
- new ListRowPresenter.SelectItemViewHolderTask(2));
- }
- });
- final Bundle[] savedState = new Bundle[1];
- // save activity state
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ 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[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
assertFalse(fragment2.isShowingHeaders());
fragment2.assertNoEntranceTransition();
@@ -816,13 +1135,7 @@
}
});
// Wait header transition finishes
- SystemClock.sleep(100);
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return !fragment.isInHeadersTransition();
- }
- });
+ waitForHeaderTransition(fragment);
// Select item 1 on row 1 in SampleRowsFragment
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
@@ -832,18 +1145,12 @@
}
});
// Save activity state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
SingleFragmentTestActivity activity2 = launchAndWaitActivity(
RowsFragmentTest.F_3ListRow2PageRow.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
final SampleRowsFragment mainFragment2 = (SampleRowsFragment) fragment2.waitPageFragment(
SampleRowsFragment.class);
@@ -896,14 +1203,7 @@
}
}
});
- SystemClock.sleep(100);
- // Wait header transition finishes
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return !fragment.isInHeadersTransition();
- }
- });
+ waitForHeaderTransition(fragment);
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
@@ -913,18 +1213,12 @@
}
});
// Save activity state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
SingleFragmentTestActivity activity2 = launchAndWaitActivity(
RowsFragmentTest.F_3ListRow2PageRow.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
SampleFragment.class);
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
index a186938..b9da794 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -19,6 +19,8 @@
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;
@@ -89,6 +91,49 @@
}
}
+ 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) {
@@ -500,6 +545,48 @@
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;
}
/**
@@ -509,14 +596,7 @@
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++);
+ ObjectAdapter listRowAdapter = createListRowAdapter();
HeaderItem header = new HeaderItem(i, "Row " + i);
adapter.add(new ListRow(header, listRowAdapter));
}
@@ -551,19 +631,250 @@
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(createListRowsAdapter());
+ 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.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.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 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)
@@ -575,27 +886,15 @@
fragment.assertExecutedEntranceTransition();
// select item 2 on row 1
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fragment.setSelectedPosition(1, true,
- new ListRowPresenter.SelectItemViewHolderTask(2));
- }
- });
+ selectAndWaitFragmentAnimation(fragment, 1, 2);
// save activity to state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
// recreate activity with saved state
SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
RowsSupportFragmentTest.F_standard.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
// validate restored activity selected row and selected item
fragment2.assertNoEntranceTransition();
@@ -706,23 +1005,47 @@
}
/**
+ * 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 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;
- }
- });
+ getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
}
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -738,19 +1061,28 @@
}
}, 100);
}
+ }
- /**
- * Util to wait PageFragment swapped.
- */
- Fragment waitPageFragment(final Class pageFragmentClass) {
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ /**
+ * 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 boolean canProceed() {
- return pageFragmentClass.isInstance(getMainFragment())
- && getMainFragment().getView() != null;
+ public void run() {
+ setAdapter(create2PageRow3ListRow());
+ startEntranceTransition();
}
- });
- return getMainFragment();
+ }, 100);
}
}
@@ -763,27 +1095,14 @@
fragment.assertExecutedEntranceTransition();
// select item 2 on row 1.
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- fragment.setSelectedPosition(1, true,
- new ListRowPresenter.SelectItemViewHolderTask(2));
- }
- });
- final Bundle[] savedState = new Bundle[1];
- // save activity state
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ 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[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
assertFalse(fragment2.isShowingHeaders());
fragment2.assertNoEntranceTransition();
@@ -819,13 +1138,7 @@
}
});
// Wait header transition finishes
- SystemClock.sleep(100);
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return !fragment.isInHeadersTransition();
- }
- });
+ waitForHeaderTransition(fragment);
// Select item 1 on row 1 in SampleRowsSupportFragment
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
@@ -835,18 +1148,12 @@
}
});
// Save activity state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
RowsSupportFragmentTest.F_3ListRow2PageRow.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
final SampleRowsSupportFragment mainFragment2 = (SampleRowsSupportFragment) fragment2.waitPageFragment(
SampleRowsSupportFragment.class);
@@ -899,14 +1206,7 @@
}
}
});
- SystemClock.sleep(100);
- // Wait header transition finishes
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return !fragment.isInHeadersTransition();
- }
- });
+ waitForHeaderTransition(fragment);
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
@@ -916,18 +1216,12 @@
}
});
// Save activity state
- final Bundle[] savedState = new Bundle[1];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- savedState[0] = activity.performSaveInstanceState();
- }
- });
+ Bundle savedState = saveActivityState(activity);
activity.finish();
SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
RowsSupportFragmentTest.F_3ListRow2PageRow.class,
- new Options().savedInstance(savedState[0]), 2000);
+ new Options().savedInstance(savedState), 2000);
final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
SampleFragment.class);
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_textview_activity.xml b/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
index 2af99ac..1538e3a 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/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/widget/AppCompatTextViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
index 933980d..bc2ad5c 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,39 @@
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.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 +61,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);
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index aa79213..17cd472 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -106,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);
}
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/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/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/layout-v23/ws_action_drawer_item_view.xml b/wear/res/layout-v23/ws_action_drawer_item_view.xml
index fc84862..a2e4640 100644
--- a/wear/res/layout-v23/ws_action_drawer_item_view.xml
+++ b/wear/res/layout-v23/ws_action_drawer_item_view.xml
@@ -15,7 +15,6 @@
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ws_action_item_background"
@@ -30,7 +29,7 @@
android:tint="?android:attr/colorBackground"
android:padding="@dimen/ws_action_drawer_item_icon_padding"
android:scaleType="fitCenter"
- tools:ignore="ContentDescription" />
+ android:importantForAccessibility="no"/>
<TextView
android:id="@+id/ws_action_drawer_item_text"
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/res/values/styles.xml b/wear/res/values/styles.xml
index 44ab0b0..5af7e6f 100644
--- a/wear/res/values/styles.xml
+++ b/wear/res/values/styles.xml
@@ -29,6 +29,7 @@
<item name="android:textSize">@dimen/ws_nav_drawer_text_size</item>
<item name="android:gravity">center</item>
<item name="android:maxLines">2</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="layout_marginTopPercent">@fraction/ws_nav_drawer_text_margin</item>
<item name="layout_marginStartPercent">@fraction/ws_nav_drawer_text_margin</item>
<item name="layout_marginEndPercent">@fraction/ws_nav_drawer_text_margin</item>
diff --git a/wear/src/main/java/android/support/wear/ambient/AmbientDelegate.java b/wear/src/main/java/android/support/wear/ambient/AmbientDelegate.java
index 4901290..8e96a02 100644
--- a/wear/src/main/java/android/support/wear/ambient/AmbientDelegate.java
+++ b/wear/src/main/java/android/support/wear/ambient/AmbientDelegate.java
@@ -19,14 +19,12 @@
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.util.Log;
import com.google.android.wearable.compat.WearableActivityController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
-import java.lang.reflect.Method;
/**
* Provides compatibility for ambient mode.
@@ -146,19 +144,6 @@
}
/**
- * Sets whether this activity's task should be moved to the front when the system exits ambient
- * mode. If true, the activity's task may be moved to the front if it was the last activity to
- * be running when ambient started, depending on how much time the system spent in ambient mode.
- */
- void setAutoResumeEnabled(boolean enabled) {
- if (mWearableController != null) {
- if (hasSetAutoResumeEnabledMethod()) {
- mWearableController.setAutoResumeEnabled(enabled);
- }
- }
- }
-
- /**
* @return {@code true} if the activity is currently in ambient.
*/
boolean isAmbient() {
@@ -177,31 +162,4 @@
mWearableController.dump(prefix, fd, writer, args);
}
}
-
- private boolean hasSetAutoResumeEnabledMethod() {
- if (!sInitAutoResumeEnabledMethod) {
- sInitAutoResumeEnabledMethod = true;
- try {
- Method method =
- WearableActivityController.class
- .getDeclaredMethod("setAutoResumeEnabled", boolean.class);
- // Proguard is sneaky -- it will actually rewrite strings it finds in addition to
- // function names. Therefore add a "." prefix to the method name check to ensure the
- // function was not renamed by proguard.
- if (!(".setAutoResumeEnabled".equals("." + method.getName()))) {
- throw new NoSuchMethodException();
- }
- sHasAutoResumeEnabledMethod = true;
- } catch (NoSuchMethodException e) {
- Log.w(
- "WearableActivity",
- "Could not find a required method for auto-resume "
- + "support, likely due to proguard optimization. Please add "
- + "com.google.android.wearable:wearable jar to the list of library "
- + "jars for your project");
- sHasAutoResumeEnabledMethod = false;
- }
- }
- return sHasAutoResumeEnabledMethod;
- }
}
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 b24f095..5db9383 100644
--- a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
+++ b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
@@ -44,7 +44,7 @@
* <p>
* <pre class="prettyprint">{@code
* AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
- * controller.setAutoResumeEnabled(true);
+ * boolean isAmbient = controller.isAmbient();
* }</pre>
*/
public final class AmbientMode extends Fragment {
@@ -117,7 +117,7 @@
* 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
@@ -126,7 +126,7 @@
* <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 =
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 8154e6b..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;
@@ -38,6 +36,7 @@
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -75,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";
@@ -140,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;
@@ -193,6 +187,16 @@
}
@Override
+ public void onDrawerOpened() {
+ if (mActionListAdapter.getItemCount() > 0) {
+ RecyclerView.ViewHolder holder = mActionList.findViewHolderForAdapterPosition(0);
+ if (holder != null && holder.itemView != null) {
+ holder.itemView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+ }
+ }
+
+ @Override
public boolean canScrollHorizontally(int direction) {
// Prevent the window from being swiped closed while it is open by saying that it can scroll
// horizontally.
@@ -412,7 +416,6 @@
CharSequence title = mActionMenu.getItem(titleAwarePosition).getTitle();
holder.textView.setText(title);
holder.textView.setContentDescription(title);
- holder.iconView.setContentDescription(title);
holder.iconView.setImageDrawable(icon);
} else if (viewHolder instanceof TitleViewHolder) {
TitleViewHolder holder = (TitleViewHolder) viewHolder;
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;
}