diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-11-30 18:18:21 -0500 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-11-30 18:18:21 -0500 |
commit | 4217cf85c20565a3446a662a7f07f26137b26b7f (patch) | |
tree | a0417b47a8cc802f6642f369fd2371165bec7b5c /android/support | |
parent | 6a65f2da209bff03cb0eb6da309710ac6ee5026d (diff) | |
download | android-28-4217cf85c20565a3446a662a7f07f26137b26b7f.tar.gz |
Import Android SDK Platform P [4477446]
/google/data/ro/projects/android/fetch_artifact \
--bid 4477446 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4477446.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: If0559643d7c328e36aafca98f0c114641d33642c
Diffstat (limited to 'android/support')
44 files changed, 1485 insertions, 648 deletions
diff --git a/android/support/LibraryGroups.java b/android/support/LibraryGroups.java index feaefbc6..19c0a927 100644 --- a/android/support/LibraryGroups.java +++ b/android/support/LibraryGroups.java @@ -27,4 +27,5 @@ public class LibraryGroups { public static final String ARCH_CORE = "android.arch.core"; public static final String PAGING = "android.arch.paging"; public static final String NAVIGATION = "android.arch.navigation"; + public static final String SLICES = "androidx.app.slice"; } diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java index 4695c45c..67a6247a 100644 --- a/android/support/car/widget/PagedListView.java +++ b/android/support/car/widget/PagedListView.java @@ -286,38 +286,6 @@ public class PagedListView extends FrameLayout { return mLayoutManager.getPosition(v); } - private void scroll(int direction) { - View focusedView = mRecyclerView.getFocusedChild(); - if (focusedView != null) { - int position = mLayoutManager.getPosition(focusedView); - int newPosition = - Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0); - if (newPosition != position) { - // newPosition/position are adapter positions. - // Convert to layout position by subtracting adapter position of view at layout - // position 0. - View childAt = mRecyclerView.getChildAt( - newPosition - mLayoutManager.getPosition(mLayoutManager.getChildAt(0))); - if (childAt != null) { - childAt.requestFocus(); - } - } - } - } - - private boolean canScroll(int direction) { - View focusedView = mRecyclerView.getFocusedChild(); - if (focusedView != null) { - int position = mLayoutManager.getPosition(focusedView); - int newPosition = - Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0); - if (newPosition != position) { - return true; - } - } - return false; - } - @NonNull public CarRecyclerView getRecyclerView() { return mRecyclerView; diff --git a/android/support/design/widget/BottomSheetBehavior.java b/android/support/design/widget/BottomSheetBehavior.java index aaa9b804..00ce8f90 100644 --- a/android/support/design/widget/BottomSheetBehavior.java +++ b/android/support/design/widget/BottomSheetBehavior.java @@ -559,7 +559,7 @@ public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behav * Gets the current state of the bottom sheet. * * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING}, - * and {@link #STATE_SETTLING}. + * {@link #STATE_SETTLING}, and {@link #STATE_HIDDEN}. */ @State public final int getState() { diff --git a/android/support/design/widget/CollapsingToolbarLayout.java b/android/support/design/widget/CollapsingToolbarLayout.java index 0051de9e..8c9b7d49 100644 --- a/android/support/design/widget/CollapsingToolbarLayout.java +++ b/android/support/design/widget/CollapsingToolbarLayout.java @@ -44,6 +44,7 @@ import android.support.v4.util.ObjectsCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.WindowInsetsCompat; +import android.support.v4.widget.ViewGroupUtils; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.AttributeSet; diff --git a/android/support/design/widget/CoordinatorLayout.java b/android/support/design/widget/CoordinatorLayout.java index 477a8d62..c45810ef 100644 --- a/android/support/design/widget/CoordinatorLayout.java +++ b/android/support/design/widget/CoordinatorLayout.java @@ -56,6 +56,8 @@ import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat.NestedScrollType; import android.support.v4.view.ViewCompat.ScrollAxis; import android.support.v4.view.WindowInsetsCompat; +import android.support.v4.widget.DirectedAcyclicGraph; +import android.support.v4.widget.ViewGroupUtils; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; diff --git a/android/support/design/widget/DirectedAcyclicGraphTest.java b/android/support/design/widget/DirectedAcyclicGraphTest.java deleted file mode 100644 index ec7687d5..00000000 --- a/android/support/design/widget/DirectedAcyclicGraphTest.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.design.widget; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.List; - -@RunWith(JUnit4.class) -public class DirectedAcyclicGraphTest { - - private DirectedAcyclicGraph<TestNode> mGraph; - - @Before - public void setup() { - mGraph = new DirectedAcyclicGraph<>(); - } - - @Test - public void test_addNode() { - final TestNode node = new TestNode("node"); - mGraph.addNode(node); - assertEquals(1, mGraph.size()); - assertTrue(mGraph.contains(node)); - } - - @Test - public void test_addNodeAgain() { - final TestNode node = new TestNode("node"); - mGraph.addNode(node); - mGraph.addNode(node); - - assertEquals(1, mGraph.size()); - assertTrue(mGraph.contains(node)); - } - - @Test - public void test_addEdge() { - final TestNode node = new TestNode("node"); - final TestNode edge = new TestNode("edge"); - - mGraph.addNode(node); - mGraph.addNode(edge); - mGraph.addEdge(node, edge); - } - - @Test(expected = IllegalArgumentException.class) - public void test_addEdgeWithNotAddedEdgeNode() { - final TestNode node = new TestNode("node"); - final TestNode edge = new TestNode("edge"); - - // Add the node, but not the edge node - mGraph.addNode(node); - - // Now add the link - mGraph.addEdge(node, edge); - } - - @Test - public void test_getIncomingEdges() { - final TestNode node = new TestNode("node"); - final TestNode edge = new TestNode("edge"); - mGraph.addNode(node); - mGraph.addNode(edge); - mGraph.addEdge(node, edge); - - final List<TestNode> incomingEdges = mGraph.getIncomingEdges(node); - assertNotNull(incomingEdges); - assertEquals(1, incomingEdges.size()); - assertEquals(edge, incomingEdges.get(0)); - } - - @Test - public void test_getOutgoingEdges() { - final TestNode node = new TestNode("node"); - final TestNode edge = new TestNode("edge"); - mGraph.addNode(node); - mGraph.addNode(edge); - mGraph.addEdge(node, edge); - - // Now assert the getOutgoingEdges returns a list which has one element (node) - final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge); - assertNotNull(outgoingEdges); - assertEquals(1, outgoingEdges.size()); - assertTrue(outgoingEdges.contains(node)); - } - - @Test - public void test_getOutgoingEdgesMultiple() { - final TestNode node1 = new TestNode("1"); - final TestNode node2 = new TestNode("2"); - final TestNode edge = new TestNode("edge"); - mGraph.addNode(node1); - mGraph.addNode(node2); - mGraph.addNode(edge); - - mGraph.addEdge(node1, edge); - mGraph.addEdge(node2, edge); - - // Now assert the getOutgoingEdges returns a list which has 2 elements (node1 & node2) - final List<TestNode> outgoingEdges = mGraph.getOutgoingEdges(edge); - assertNotNull(outgoingEdges); - assertEquals(2, outgoingEdges.size()); - assertTrue(outgoingEdges.contains(node1)); - assertTrue(outgoingEdges.contains(node2)); - } - - @Test - public void test_hasOutgoingEdges() { - final TestNode node = new TestNode("node"); - final TestNode edge = new TestNode("edge"); - mGraph.addNode(node); - mGraph.addNode(edge); - - // There is no edge currently and assert that fact - assertFalse(mGraph.hasOutgoingEdges(edge)); - // Now add the edge - mGraph.addEdge(node, edge); - // and assert that the methods returns true; - assertTrue(mGraph.hasOutgoingEdges(edge)); - } - - @Test - public void test_clear() { - final TestNode node1 = new TestNode("1"); - final TestNode node2 = new TestNode("2"); - final TestNode edge = new TestNode("edge"); - mGraph.addNode(node1); - mGraph.addNode(node2); - mGraph.addNode(edge); - - // Now clear the graph - mGraph.clear(); - - // Now assert the graph is empty and that contains returns false - assertEquals(0, mGraph.size()); - assertFalse(mGraph.contains(node1)); - assertFalse(mGraph.contains(node2)); - assertFalse(mGraph.contains(edge)); - } - - @Test - public void test_getSortedList() { - final TestNode node1 = new TestNode("A"); - final TestNode node2 = new TestNode("B"); - final TestNode node3 = new TestNode("C"); - final TestNode node4 = new TestNode("D"); - - // Now we'll add the nodes - mGraph.addNode(node1); - mGraph.addNode(node2); - mGraph.addNode(node3); - mGraph.addNode(node4); - - // Now we'll add edges so that 4 <- 2, 2 <- 3, 3 <- 1 (where <- denotes a dependency) - mGraph.addEdge(node4, node2); - mGraph.addEdge(node2, node3); - mGraph.addEdge(node3, node1); - - final List<TestNode> sorted = mGraph.getSortedList(); - // Assert that it is the correct size - assertEquals(4, sorted.size()); - // Assert that all of the nodes are present and in their sorted order - assertEquals(node1, sorted.get(0)); - assertEquals(node3, sorted.get(1)); - assertEquals(node2, sorted.get(2)); - assertEquals(node4, sorted.get(3)); - } - - private static class TestNode { - private final String mLabel; - - TestNode(@NonNull String label) { - mLabel = label; - } - - @Override - public String toString() { - return "TestNode: " + mLabel; - } - } - -} diff --git a/android/support/design/widget/FloatingActionButton.java b/android/support/design/widget/FloatingActionButton.java index b9388366..f37b3798 100644 --- a/android/support/design/widget/FloatingActionButton.java +++ b/android/support/design/widget/FloatingActionButton.java @@ -36,6 +36,7 @@ import android.support.annotation.VisibleForTesting; import android.support.design.R; import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener; import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewGroupUtils; import android.support.v7.widget.AppCompatImageHelper; import android.util.AttributeSet; import android.util.Log; @@ -116,6 +117,11 @@ public class FloatingActionButton extends VisibilityAwareImageButton { public static final int SIZE_AUTO = -1; /** + * Indicates that FloatingActionButton should not have a custom size. + */ + public static final int NO_CUSTOM_SIZE = 0; + + /** * The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal. */ private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470; @@ -132,6 +138,7 @@ public class FloatingActionButton extends VisibilityAwareImageButton { private int mBorderWidth; private int mRippleColor; private int mSize; + private int mCustomSize; int mImagePadding; private int mMaxImageSize; @@ -164,6 +171,8 @@ public class FloatingActionButton extends VisibilityAwareImageButton { R.styleable.FloatingActionButton_backgroundTintMode, -1), null); mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO); + mCustomSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabCustomSize, + 0); mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0); final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); final float pressedTranslationZ = a.getDimension( @@ -430,12 +439,41 @@ public class FloatingActionButton extends VisibilityAwareImageButton { }; } + /** + * Sets the size of the button to be a custom value in pixels. If set to + * {@link #NO_CUSTOM_SIZE}, custom size will not be used and size will be calculated according + * to {@link #setSize(int)} method. + * + * @param size preferred size in pixels, or zero + * + * @attr ref android.support.design.R.styleable#FloatingActionButton_fabCustomSize + */ + public void setCustomSize(int size) { + if (size < 0) { + throw new IllegalArgumentException("Custom size should be non-negative."); + } + mCustomSize = size; + } + + /** + * Returns the custom size for this button. + * + * @return size in pixels, or {@link #NO_CUSTOM_SIZE} + */ + public int getCustomSize() { + return mCustomSize; + } + int getSizeDimension() { return getSizeDimension(mSize); } private int getSizeDimension(@Size final int size) { final Resources res = getResources(); + // If custom size is set, return it + if (mCustomSize != NO_CUSTOM_SIZE) { + return mCustomSize; + } switch (size) { case SIZE_AUTO: // If we're set to auto, grab the size from resources and refresh diff --git a/android/support/design/widget/TextInputEditText.java b/android/support/design/widget/TextInputEditText.java index 7235ec22..ee6c32cd 100644 --- a/android/support/design/widget/TextInputEditText.java +++ b/android/support/design/widget/TextInputEditText.java @@ -18,6 +18,7 @@ package android.support.design.widget; import android.content.Context; import android.support.v7.widget.AppCompatEditText; +import android.support.v7.widget.WithHint; import android.util.AttributeSet; import android.view.View; import android.view.ViewParent; @@ -48,12 +49,12 @@ public class TextInputEditText extends AppCompatEditText { public InputConnection onCreateInputConnection(EditorInfo outAttrs) { final InputConnection ic = super.onCreateInputConnection(outAttrs); if (ic != null && outAttrs.hintText == null) { - // If we don't have a hint and our parent is a TextInputLayout, use it's hint for the + // If we don't have a hint and our parent implements WithHint, use its hint for the // EditorInfo. This allows us to display a hint in 'extract mode'. ViewParent parent = getParent(); while (parent instanceof View) { - if (parent instanceof TextInputLayout) { - outAttrs.hintText = ((TextInputLayout) parent).getHint(); + if (parent instanceof WithHint) { + outAttrs.hintText = ((WithHint) parent).getHint(); break; } parent = parent.getParent(); diff --git a/android/support/design/widget/TextInputLayout.java b/android/support/design/widget/TextInputLayout.java index c9e8010d..0540678e 100644 --- a/android/support/design/widget/TextInputLayout.java +++ b/android/support/design/widget/TextInputLayout.java @@ -49,10 +49,12 @@ import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.Space; import android.support.v4.widget.TextViewCompat; +import android.support.v4.widget.ViewGroupUtils; import android.support.v7.content.res.AppCompatResources; import android.support.v7.widget.AppCompatDrawableManager; import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.TintTypedArray; +import android.support.v7.widget.WithHint; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -113,7 +115,7 @@ import android.widget.TextView; * may not return the TextInputLayout itself, but rather an intermediate View. If you need * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}. */ -public class TextInputLayout extends LinearLayout { +public class TextInputLayout extends LinearLayout implements WithHint { private static final int ANIMATION_DURATION = 200; private static final int INVALID_MAX_LENGTH = -1; @@ -497,6 +499,7 @@ public class TextInputLayout extends LinearLayout { * * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint */ + @Override @Nullable public CharSequence getHint() { return mHintEnabled ? mHint : null; diff --git a/android/support/doclava/DoclavaJavadocOptionFileOption.java b/android/support/doclava/DoclavaJavadocOptionFileOption.java deleted file mode 100644 index db3f3188..00000000 --- a/android/support/doclava/DoclavaJavadocOptionFileOption.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.doclava; - -import org.gradle.external.javadoc.internal.AbstractJavadocOptionFileOption; -import org.gradle.external.javadoc.internal.JavadocOptionFileWriterContext; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * This class is used to hold complex argument(s) to doclava - */ -public class DoclavaJavadocOptionFileOption extends - AbstractJavadocOptionFileOption<Iterable<String>> { - - public DoclavaJavadocOptionFileOption(String option) { - super(option, null); - } - - public DoclavaJavadocOptionFileOption(String option, Iterable<String> value) { - super(option, value); - } - - @Override - public void write(JavadocOptionFileWriterContext writerContext) throws IOException { - writerContext.writeOptionHeader(getOption()); - - final Iterable<String> args = getValue(); - if (args != null) { - final Iterator<String> iter = args.iterator(); - while (true) { - writerContext.writeValue(iter.next()); - if (!iter.hasNext()) { - break; - } - writerContext.write(" "); - } - } - - writerContext.newLine(); - } - - /** - * @return a deep copy of the option - */ - public DoclavaJavadocOptionFileOption duplicate() { - final Iterable<String> value = getValue(); - final ArrayList<String> valueCopy; - if (value != null) { - valueCopy = new ArrayList<>(); - for (String item : value) { - valueCopy.add(item); - } - } else { - valueCopy = null; - } - return new DoclavaJavadocOptionFileOption(getOption(), valueCopy); - } -} diff --git a/android/support/graphics/drawable/VectorDrawableCompat.java b/android/support/graphics/drawable/VectorDrawableCompat.java index 2c7ae41c..a34fe2b8 100644 --- a/android/support/graphics/drawable/VectorDrawableCompat.java +++ b/android/support/graphics/drawable/VectorDrawableCompat.java @@ -173,6 +173,10 @@ import java.util.Stack; * <dd>Sets the lineJoin for a stroked path: miter,round,bevel. Default is miter.</dd> * <dt><code>android:strokeMiterLimit</code></dt> * <dd>Sets the Miter limit for a stroked path. Default is 4.</dd> + * <dt><code>android:fillType</code></dt> + * <dd>Sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the + * same as SVG's "fill-rule" properties. Default is nonZero. For more details, see + * <a href="https://www.w3.org/TR/SVG/painting.html#FillRuleProperty">FillRuleProperty</a></dd> * </dl></dd> * </dl> * diff --git a/android/support/mediacompat/testlib/MediaBrowserConstants.java b/android/support/mediacompat/testlib/MediaBrowserConstants.java index 86024d90..f961308d 100644 --- a/android/support/mediacompat/testlib/MediaBrowserConstants.java +++ b/android/support/mediacompat/testlib/MediaBrowserConstants.java @@ -31,14 +31,16 @@ public class MediaBrowserConstants { public static final int SET_SESSION_TOKEN = 7; public static final String MEDIA_ID_ROOT = "test_media_id_root"; - - public static final String EXTRAS_KEY = "test_extras_key"; - public static final String EXTRAS_VALUE = "test_extras_value"; - public static final String MEDIA_ID_INVALID = "test_media_id_invalid"; public static final String MEDIA_ID_CHILDREN_DELAYED = "test_media_id_children_delayed"; public static final String MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED = "test_media_id_on_load_item_not_implemented"; + public static final String MEDIA_ID_INCLUDE_METADATA = "test_media_id_include_metadata"; + + public static final String EXTRAS_KEY = "test_extras_key"; + public static final String EXTRAS_VALUE = "test_extras_value"; + + public static final String MEDIA_METADATA = "test_media_metadata"; public static final String SEARCH_QUERY = "children_2"; public static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result"; diff --git a/android/support/mediacompat/testlib/MediaControllerConstants.java b/android/support/mediacompat/testlib/MediaControllerConstants.java index 5fa086b3..49788882 100644 --- a/android/support/mediacompat/testlib/MediaControllerConstants.java +++ b/android/support/mediacompat/testlib/MediaControllerConstants.java @@ -26,6 +26,8 @@ public class MediaControllerConstants { public static final int ADD_QUEUE_ITEM = 202; public static final int ADD_QUEUE_ITEM_WITH_INDEX = 203; public static final int REMOVE_QUEUE_ITEM = 204; + public static final int SET_VOLUME_TO = 205; + public static final int ADJUST_VOLUME = 206; // TransportControls methods. public static final int PLAY = 301; diff --git a/android/support/mediacompat/testlib/MediaSessionConstants.java b/android/support/mediacompat/testlib/MediaSessionConstants.java index cbdccc1b..c0a64d4d 100644 --- a/android/support/mediacompat/testlib/MediaSessionConstants.java +++ b/android/support/mediacompat/testlib/MediaSessionConstants.java @@ -51,6 +51,8 @@ public class MediaSessionConstants { public static final long TEST_QUEUE_ID_2 = 20L; public static final String TEST_MEDIA_ID_1 = "media_id_1"; public static final String TEST_MEDIA_ID_2 = "media_id_2"; + public static final String TEST_MEDIA_TITLE_1 = "media_title_1"; + public static final String TEST_MEDIA_TITLE_2 = "media_title_2"; public static final long TEST_ACTION = 55L; public static final int TEST_ERROR_CODE = 0x3; diff --git a/android/support/mediacompat/testlib/VersionConstants.java b/android/support/mediacompat/testlib/VersionConstants.java index 6533ee17..4b217b10 100644 --- a/android/support/mediacompat/testlib/VersionConstants.java +++ b/android/support/mediacompat/testlib/VersionConstants.java @@ -22,4 +22,7 @@ package android.support.mediacompat.testlib; public class VersionConstants { public static final String KEY_CLIENT_VERSION = "client_version"; public static final String KEY_SERVICE_VERSION = "service_version"; + + public static final String VERSION_TOT = "tot"; + public static final String VERSION_PREVIOUS = "previous"; } diff --git a/android/support/mediacompat/testlib/util/IntentUtil.java b/android/support/mediacompat/testlib/util/IntentUtil.java index bbf97524..8d58a6ff 100644 --- a/android/support/mediacompat/testlib/util/IntentUtil.java +++ b/android/support/mediacompat/testlib/util/IntentUtil.java @@ -30,12 +30,13 @@ import java.util.ArrayList; */ public class IntentUtil { + public static final String SERVICE_PACKAGE_NAME = "android.support.mediacompat.service.test"; + public static final String CLIENT_PACKAGE_NAME = "android.support.mediacompat.client.test"; + public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName( - "android.support.mediacompat.service.test", - "android.support.mediacompat.service.ServiceBroadcastReceiver"); + SERVICE_PACKAGE_NAME, "android.support.mediacompat.service.ServiceBroadcastReceiver"); public static final ComponentName CLIENT_RECEIVER_COMPONENT_NAME = new ComponentName( - "android.support.mediacompat.client.test", - "android.support.mediacompat.client.ClientBroadcastReceiver"); + CLIENT_PACKAGE_NAME, "android.support.mediacompat.client.ClientBroadcastReceiver"); public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD = "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"; diff --git a/android/support/text/emoji/EmojiCompat.java b/android/support/text/emoji/EmojiCompat.java index f258c12d..5436aa20 100644 --- a/android/support/text/emoji/EmojiCompat.java +++ b/android/support/text/emoji/EmojiCompat.java @@ -221,6 +221,7 @@ public class EmojiCompat { * * @see EmojiCompat.Config */ + @SuppressWarnings("GuardedBy") public static EmojiCompat init(@NonNull final Config config) { if (sInstance == null) { synchronized (sInstanceLock) { @@ -238,6 +239,7 @@ public class EmojiCompat { * * @hide */ + @SuppressWarnings("GuardedBy") @RestrictTo(LIBRARY_GROUP) @VisibleForTesting public static EmojiCompat reset(@NonNull final Config config) { @@ -252,6 +254,7 @@ public class EmojiCompat { * * @hide */ + @SuppressWarnings("GuardedBy") @RestrictTo(LIBRARY_GROUP) @VisibleForTesting public static EmojiCompat reset(final EmojiCompat emojiCompat) { diff --git a/android/support/text/emoji/MetadataListReader.java b/android/support/text/emoji/MetadataListReader.java index 6034726d..1008c171 100644 --- a/android/support/text/emoji/MetadataListReader.java +++ b/android/support/text/emoji/MetadataListReader.java @@ -275,7 +275,7 @@ class MetadataListReader { @Override public void skip(int numOfBytes) throws IOException { while (numOfBytes > 0) { - long skipped = mInputStream.skip(numOfBytes); + int skipped = (int) mInputStream.skip(numOfBytes); if (skipped < 1) { throw new IOException("Skip didn't move at least 1 byte forward"); } diff --git a/android/support/text/emoji/widget/EmojiEditableFactory.java b/android/support/text/emoji/widget/EmojiEditableFactory.java index 9793c9da..20cde4f9 100644 --- a/android/support/text/emoji/widget/EmojiEditableFactory.java +++ b/android/support/text/emoji/widget/EmojiEditableFactory.java @@ -55,6 +55,7 @@ final class EmojiEditableFactory extends Editable.Factory { } } + @SuppressWarnings("GuardedBy") public static Editable.Factory getInstance() { if (sInstance == null) { synchronized (sInstanceLock) { diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index d7020e91..9d159eca 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -2726,40 +2726,6 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } } - // Observer is registered on Adapter to invalidate saved instance state - final RecyclerView.AdapterDataObserver mObServer = new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - mChildrenStates.clear(); - } - - @Override - public void onItemRangeChanged(int positionStart, int itemCount) { - if (DEBUG) { - Log.v(getTag(), "onItemRangeChanged positionStart " - + positionStart + " itemCount " + itemCount); - } - for (int i = positionStart, end = positionStart + itemCount; i < end; i++) { - mChildrenStates.remove(i); - } - } - - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - mChildrenStates.clear(); - } - - @Override - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - mChildrenStates.clear(); - } - - @Override - public void onItemRangeRemoved(int positionStart, int itemCount) { - mChildrenStates.clear(); - } - }; - @Override public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart " @@ -2771,12 +2737,14 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPositionOffset += itemCount; } } + mChildrenStates.clear(); } @Override public void onItemsChanged(RecyclerView recyclerView) { if (DEBUG) Log.v(getTag(), "onItemsChanged"); mFocusPositionOffset = 0; + mChildrenStates.clear(); } @Override @@ -2797,6 +2765,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } } } + mChildrenStates.clear(); } @Override @@ -2817,6 +2786,16 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPositionOffset += itemCount; } } + mChildrenStates.clear(); + } + + @Override + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { + if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart " + + positionStart + " itemCount " + itemCount); + for (int i = positionStart, end = positionStart + itemCount; i < end; i++) { + mChildrenStates.remove(i); + } } @Override @@ -3515,16 +3494,12 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPosition = NO_POSITION; mFocusPositionOffset = 0; mChildrenStates.clear(); - oldAdapter.unregisterAdapterDataObserver(mObServer); } if (newAdapter instanceof FacetProviderAdapter) { mFacetProviderAdapter = (FacetProviderAdapter) newAdapter; } else { mFacetProviderAdapter = null; } - if (newAdapter != null) { - newAdapter.registerAdapterDataObserver(mObServer); - } super.onAdapterChanged(oldAdapter, newAdapter); } diff --git a/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/android/support/v4/graphics/TypefaceCompatApi26Impl.java index 00e31a1a..f23ac0d4 100644 --- a/android/support/v4/graphics/TypefaceCompatApi26Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi26Impl.java @@ -189,10 +189,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { /** * Call FontFamily#abortCreation() */ - private boolean abortCreation(Object family) { + private void abortCreation(Object family) { try { - Boolean result = (Boolean) mAbortCreation.invoke(family); - return result.booleanValue(); + mAbortCreation.invoke(family); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } diff --git a/android/support/v4/media/MediaBrowserCompat.java b/android/support/v4/media/MediaBrowserCompat.java index 7adf7d78..1b279253 100644 --- a/android/support/v4/media/MediaBrowserCompat.java +++ b/android/support/v4/media/MediaBrowserCompat.java @@ -41,10 +41,12 @@ import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; +import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; +import static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_2; import android.content.ComponentName; import android.content.Context; @@ -1581,6 +1583,7 @@ public final class MediaBrowserCompat { protected final CallbackHandler mHandler = new CallbackHandler(this); private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); + protected int mServiceVersion; protected ServiceBinderWrapper mServiceBinderWrapper; protected Messenger mCallbacksMessenger; private MediaSessionCompat.Token mMediaSessionToken; @@ -1850,6 +1853,7 @@ public final class MediaBrowserCompat { if (extras == null) { return; } + mServiceVersion = extras.getInt(EXTRA_SERVICE_VERSION, 0); IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER); if (serviceBinder != null) { mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints); @@ -1956,7 +1960,9 @@ public final class MediaBrowserCompat { @Override public void subscribe(@NonNull String parentId, @Nullable Bundle options, @NonNull SubscriptionCallback callback) { - if (mServiceBinderWrapper == null) { + // From service v2, we use compat code when subscribing. + // This is to prevent ClassNotFoundException when options has Parcelable in it. + if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) { if (options == null) { MediaBrowserCompatApi21.subscribe( mBrowserObj, parentId, callback.mSubscriptionCallbackObj); @@ -1971,7 +1977,9 @@ public final class MediaBrowserCompat { @Override public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { - if (mServiceBinderWrapper == null) { + // From service v2, we use compat code when subscribing. + // This is to prevent ClassNotFoundException when options has Parcelable in it. + if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) { if (callback == null) { MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); } else { diff --git a/android/support/v4/media/MediaBrowserProtocol.java b/android/support/v4/media/MediaBrowserProtocol.java index 7c23d261..8ed152df 100644 --- a/android/support/v4/media/MediaBrowserProtocol.java +++ b/android/support/v4/media/MediaBrowserProtocol.java @@ -45,7 +45,15 @@ class MediaBrowserProtocol { * MediaBrowserServiceCompat. */ public static final int SERVICE_VERSION_1 = 1; - public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_1; + + /** + * To prevent ClassNotFoundException from Parcelables, MediaBrowser(Service)Compat tries to + * avoid using framework code as much as possible (b/62648808). For backward compatibility, + * service v2 is introduced so that the browser can distinguish whether the service supports + * subscribing through compat code. + */ + public static final int SERVICE_VERSION_2 = 2; + public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_2; /* * Messages sent from the media browser service compat to the media browser compat. diff --git a/android/support/v4/media/MediaBrowserServiceCompat.java b/android/support/v4/media/MediaBrowserServiceCompat.java index debc66e8..27bf0e30 100644 --- a/android/support/v4/media/MediaBrowserServiceCompat.java +++ b/android/support/v4/media/MediaBrowserServiceCompat.java @@ -278,28 +278,8 @@ public abstract class MediaBrowserServiceCompat extends Service { @Override public void notifyChildrenChanged(final String parentId, final Bundle options) { - if (mMessenger == null) { - MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); - } else { - mHandler.post(new Runnable() { - @Override - public void run() { - for (IBinder binder : mConnections.keySet()) { - ConnectionRecord connection = mConnections.get(binder); - List<Pair<IBinder, Bundle>> callbackList = - connection.subscriptions.get(parentId); - if (callbackList != null) { - for (Pair<IBinder, Bundle> callback : callbackList) { - if (MediaBrowserCompatUtils.hasDuplicatedItems( - options, callback.second)) { - performLoadChildren(parentId, connection, callback.second); - } - } - } - } - } - }); - } + notifyChildrenChangedForFramework(parentId, options); + notifyChildrenChangedForCompat(parentId, options); } @Override @@ -373,6 +353,31 @@ public abstract class MediaBrowserServiceCompat extends Service { }; MediaBrowserServiceCompat.this.onLoadChildren(parentId, result); } + + void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { + MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); + } + + void notifyChildrenChangedForCompat(final String parentId, final Bundle options) { + mHandler.post(new Runnable() { + @Override + public void run() { + for (IBinder binder : mConnections.keySet()) { + ConnectionRecord connection = mConnections.get(binder); + List<Pair<IBinder, Bundle>> callbackList = + connection.subscriptions.get(parentId); + if (callbackList != null) { + for (Pair<IBinder, Bundle> callback : callbackList) { + if (MediaBrowserCompatUtils.hasDuplicatedItems( + options, callback.second)) { + performLoadChildren(parentId, connection, callback.second); + } + } + } + } + } + }); + } } @RequiresApi(23) @@ -421,20 +426,6 @@ public abstract class MediaBrowserServiceCompat extends Service { } @Override - public void notifyChildrenChanged(final String parentId, final Bundle options) { - if (mMessenger == null) { - if (options == null) { - MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); - } else { - MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId, - options); - } - } else { - super.notifyChildrenChanged(parentId, options); - } - } - - @Override public void onLoadChildren(String parentId, final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options) { final Result<List<MediaBrowserCompat.MediaItem>> result @@ -470,6 +461,16 @@ public abstract class MediaBrowserServiceCompat extends Service { } return MediaBrowserServiceCompatApi26.getBrowserRootHints(mServiceObj); } + + @Override + void notifyChildrenChangedForFramework(final String parentId, final Bundle options) { + if (options != null) { + MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId, + options); + } else { + super.notifyChildrenChangedForFramework(parentId, options); + } + } } private final class ServiceHandler extends Handler { diff --git a/android/support/v4/view/NestedScrollingParent2.java b/android/support/v4/view/NestedScrollingParent2.java index db41c461..2ab463ec 100644 --- a/android/support/v4/view/NestedScrollingParent2.java +++ b/android/support/v4/view/NestedScrollingParent2.java @@ -18,7 +18,6 @@ package android.support.v4.view; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat.NestedScrollType; import android.support.v4.view.ViewCompat.ScrollAxis; import android.view.MotionEvent; @@ -144,7 +143,7 @@ public interface NestedScrollingParent2 extends NestedScrollingParent { * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent * @param type the type of input which cause this scroll event */ - void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, + void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type); } diff --git a/android/support/v4/view/PagerAdapter.java b/android/support/v4/view/PagerAdapter.java index a8fb099c..af8e076f 100644 --- a/android/support/v4/view/PagerAdapter.java +++ b/android/support/v4/view/PagerAdapter.java @@ -131,6 +131,7 @@ public abstract class PagerAdapter { /** * Called to inform the adapter of which item is currently considered to * be the "primary", that is the one show to the user as the current page. + * This method will not be invoked when the adapter contains no items. * * @param container The containing View from which the page will be removed. * @param position The page position that is now the primary. diff --git a/android/support/v4/view/ViewPager.java b/android/support/v4/view/ViewPager.java index 36d8696c..350fe955 100644 --- a/android/support/v4/view/ViewPager.java +++ b/android/support/v4/view/ViewPager.java @@ -1224,6 +1224,8 @@ public class ViewPager extends ViewGroup { } calculatePageOffsets(curItem, curIndex, oldCurInfo); + + mAdapter.setPrimaryItem(this, mCurItem, curItem.object); } if (DEBUG) { @@ -1233,8 +1235,6 @@ public class ViewPager extends ViewGroup { } } - mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); - mAdapter.finishUpdate(this); // Check width measurement of current pages and drawing sort order. diff --git a/android/support/design/widget/DirectedAcyclicGraph.java b/android/support/v4/widget/DirectedAcyclicGraph.java index 85a32cd5..83c62c06 100644 --- a/android/support/design/widget/DirectedAcyclicGraph.java +++ b/android/support/v4/widget/DirectedAcyclicGraph.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -14,10 +14,13 @@ * limitations under the License. */ -package android.support.design.widget; +package android.support.v4.widget; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; import android.support.v4.util.Pools; import android.support.v4.util.SimpleArrayMap; @@ -27,8 +30,13 @@ import java.util.List; /** * A class which represents a simple directed acyclic graph. + * + * @param <T> Class for the data objects of this graph. + * + * @hide */ -final class DirectedAcyclicGraph<T> { +@RestrictTo(LIBRARY) +public final class DirectedAcyclicGraph<T> { private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10); private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>(); @@ -42,7 +50,7 @@ final class DirectedAcyclicGraph<T> { * * @param node the node to add */ - void addNode(@NonNull T node) { + public void addNode(@NonNull T node) { if (!mGraph.containsKey(node)) { mGraph.put(node, null); } @@ -51,7 +59,7 @@ final class DirectedAcyclicGraph<T> { /** * Returns true if the node is already present in the graph, false otherwise. */ - boolean contains(@NonNull T node) { + public boolean contains(@NonNull T node) { return mGraph.containsKey(node); } @@ -64,7 +72,7 @@ final class DirectedAcyclicGraph<T> { * @param node the parent node * @param incomingEdge the node which has is an incoming edge to {@code node} */ - void addEdge(@NonNull T node, @NonNull T incomingEdge) { + public void addEdge(@NonNull T node, @NonNull T incomingEdge) { if (!mGraph.containsKey(node) || !mGraph.containsKey(incomingEdge)) { throw new IllegalArgumentException("All nodes must be present in the graph before" + " being added as an edge"); @@ -86,7 +94,7 @@ final class DirectedAcyclicGraph<T> { * @return a list containing any incoming edges, or null if there are none. */ @Nullable - List getIncomingEdges(@NonNull T node) { + public List getIncomingEdges(@NonNull T node) { return mGraph.get(node); } @@ -97,7 +105,7 @@ final class DirectedAcyclicGraph<T> { * @return a list containing any outgoing edges, or null if there are none. */ @Nullable - List<T> getOutgoingEdges(@NonNull T node) { + public List<T> getOutgoingEdges(@NonNull T node) { ArrayList<T> result = null; for (int i = 0, size = mGraph.size(); i < size; i++) { ArrayList<T> edges = mGraph.valueAt(i); @@ -111,7 +119,14 @@ final class DirectedAcyclicGraph<T> { return result; } - boolean hasOutgoingEdges(@NonNull T node) { + /** + * Checks whether we have any outgoing edges for the given node (i.e. nodes which have + * an incoming edge from the given node). + * + * @return <code>true</code> if the node has any outgoing edges, <code>false</code> + * otherwise. + */ + public boolean hasOutgoingEdges(@NonNull T node) { for (int i = 0, size = mGraph.size(); i < size; i++) { ArrayList<T> edges = mGraph.valueAt(i); if (edges != null && edges.contains(node)) { @@ -124,7 +139,7 @@ final class DirectedAcyclicGraph<T> { /** * Clears the internal graph, and releases resources to pools. */ - void clear() { + public void clear() { for (int i = 0, size = mGraph.size(); i < size; i++) { ArrayList<T> edges = mGraph.valueAt(i); if (edges != null) { @@ -143,7 +158,7 @@ final class DirectedAcyclicGraph<T> { * of the graph. The node at the end of the list will have no dependencies on other nodes.</p> */ @NonNull - ArrayList<T> getSortedList() { + public ArrayList<T> getSortedList() { mSortResult.clear(); mSortTmpMarked.clear(); @@ -198,4 +213,4 @@ final class DirectedAcyclicGraph<T> { list.clear(); mListPool.release(list); } -}
\ No newline at end of file +} diff --git a/android/support/design/widget/ViewGroupUtils.java b/android/support/v4/widget/ViewGroupUtils.java index 5d8b5c74..986b4c20 100644 --- a/android/support/design/widget/ViewGroupUtils.java +++ b/android/support/v4/widget/ViewGroupUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * 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. @@ -14,16 +14,23 @@ * limitations under the License. */ -package android.support.design.widget; +package android.support.v4.widget; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; +import android.support.annotation.RestrictTo; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -class ViewGroupUtils { +/** + * @hide + */ +@RestrictTo(LIBRARY) +public class ViewGroupUtils { private static final ThreadLocal<Matrix> sMatrix = new ThreadLocal<>(); private static final ThreadLocal<RectF> sRectF = new ThreadLocal<>(); @@ -65,7 +72,7 @@ class ViewGroupUtils { * @param descendant descendant view to reference * @param out rect to set to the bounds of the descendant view */ - static void getDescendantRect(ViewGroup parent, View descendant, Rect out) { + public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) { out.set(0, 0, descendant.getWidth(), descendant.getHeight()); offsetDescendantRect(parent, descendant, out); } diff --git a/android/support/v7/preference/CollapsiblePreferenceGroupController.java b/android/support/v7/preference/CollapsiblePreferenceGroupController.java index e15ca18f..b63ff75b 100644 --- a/android/support/v7/preference/CollapsiblePreferenceGroupController.java +++ b/android/support/v7/preference/CollapsiblePreferenceGroupController.java @@ -166,7 +166,7 @@ final class CollapsiblePreferenceGroupController CharSequence summary = null; for (int i = collapsedIndex; i < flattenedPreferenceList.size(); i++) { final Preference preference = flattenedPreferenceList.get(i); - if (preference instanceof PreferenceGroup) { + if (preference instanceof PreferenceGroup || !preference.isVisible()) { continue; } final CharSequence title = preference.getTitle(); diff --git a/android/support/v7/util/SortedList.java b/android/support/v7/util/SortedList.java index af000a1e..bd07b01e 100644 --- a/android/support/v7/util/SortedList.java +++ b/android/support/v7/util/SortedList.java @@ -16,6 +16,7 @@ package android.support.v7.util; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.lang.reflect.Array; @@ -51,17 +52,23 @@ public class SortedList<T> { T[] mData; /** - * A copy of the previous list contents used during the merge phase of addAll. + * A reference to the previous set of data that is kept during a mutation operation (addAll or + * replaceAll). */ private T[] mOldData; + + /** + * The current index into mOldData that has not yet been processed during a mutation operation + * (addAll or replaceAll). + */ private int mOldDataStart; private int mOldDataSize; /** - * The size of the valid portion of mData during the merge phase of addAll. + * The current index into the new data that has not yet been processed during a mutation + * operation (addAll or replaceAll). */ - private int mMergedSize; - + private int mNewDataStart; /** * The callback instance that controls the behavior of the SortedList and get notified when @@ -133,7 +140,7 @@ public class SortedList<T> { * @see Callback#areContentsTheSame(Object, Object)} */ public int add(T item) { - throwIfMerging(); + throwIfInMutationOperation(); return add(item, true); } @@ -142,30 +149,30 @@ public class SortedList<T> { * except the callback events may be in a different order/granularity since addAll can batch * them for better performance. * <p> - * If allowed, may modify the input array and even take the ownership over it in order - * to avoid extra memory allocation during sorting and deduplication. - * </p> + * If allowed, will reference the input array during, and possibly after, the operation to avoid + * extra memory allocation, in which case you should not continue to reference or modify the + * array yourself. + * <p> * @param items Array of items to be added into the list. - * @param mayModifyInput If true, SortedList is allowed to modify the input. - * @see SortedList#addAll(Object[] items) + * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the + * input array. + * @see SortedList#addAll(T[] items) */ public void addAll(T[] items, boolean mayModifyInput) { - throwIfMerging(); + throwIfInMutationOperation(); if (items.length == 0) { return; } + if (mayModifyInput) { addAllInternal(items); } else { - T[] copy = (T[]) Array.newInstance(mTClass, items.length); - System.arraycopy(items, 0, copy, 0, items.length); - addAllInternal(copy); + addAllInternal(copyArray(items)); } - } /** - * Adds the given items to the list. Does not modify the input. + * Adds the given items to the list. Does not modify or retain the input. * * @see SortedList#addAll(T[] items, boolean mayModifyInput) * @@ -176,7 +183,7 @@ public class SortedList<T> { } /** - * Adds the given items to the list. Does not modify the input. + * Adds the given items to the list. Does not modify or retain the input. * * @see SortedList#addAll(T[] items, boolean mayModifyInput) * @@ -187,27 +194,134 @@ public class SortedList<T> { addAll(items.toArray(copy), true); } - private void addAllInternal(T[] newItems) { - final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); - if (forceBatchedUpdates) { - beginBatchedUpdates(); + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. + * <p> + * If allowed, will reference the input array during, and possibly after, the operation to avoid + * extra memory allocation, in which case you should not continue to reference or modify the + * array yourself. + * <p> + * Note: this method does not detect moves or dispatch + * {@link ListUpdateCallback#onMoved(int, int)} events. It instead treats moves as a remove + * followed by an add and therefore dispatches {@link ListUpdateCallback#onRemoved(int, int)} + * and {@link ListUpdateCallback#onRemoved(int, int)} events. See {@link DiffUtil} if you want + * your implementation to dispatch move events. + * <p> + * @param items Array of items to replace current items. + * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the + * input array. + * @see #replaceAll(T[]) + */ + public void replaceAll(@NonNull T[] items, boolean mayModifyInput) { + throwIfInMutationOperation(); + + if (mayModifyInput) { + replaceAllInternal(items); + } else { + replaceAllInternal(copyArray(items)); } + } - mOldData = mData; - mOldDataStart = 0; - mOldDataSize = mSize; + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. Does not modify or retain the input. + * + * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. + */ + public void replaceAll(@NonNull T... items) { + replaceAll(items, false); + } - Arrays.sort(newItems, mCallback); // Arrays.sort is stable. + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. Does not modify or retain the input. + * + * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. + */ + public void replaceAll(@NonNull Collection<T> items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.size()); + replaceAll(items.toArray(copy), true); + } + + private void addAllInternal(T[] newItems) { + if (newItems.length < 1) { + return; + } + + final int newSize = sortAndDedup(newItems); - final int newSize = deduplicate(newItems); if (mSize == 0) { mData = newItems; mSize = newSize; - mMergedSize = newSize; mCallback.onInserted(0, newSize); } else { merge(newItems, newSize); } + } + + private void replaceAllInternal(@NonNull T[] newData) { + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + if (forceBatchedUpdates) { + beginBatchedUpdates(); + } + + mOldDataStart = 0; + mOldDataSize = mSize; + mOldData = mData; + + mNewDataStart = 0; + int newSize = sortAndDedup(newData); + mData = (T[]) Array.newInstance(mTClass, newSize); + + while (mNewDataStart < newSize || mOldDataStart < mOldDataSize) { + if (mOldDataStart >= mOldDataSize) { + int insertIndex = mNewDataStart; + int itemCount = newSize - mNewDataStart; + System.arraycopy(newData, insertIndex, mData, insertIndex, itemCount); + mNewDataStart += itemCount; + mSize += itemCount; + mCallback.onInserted(insertIndex, itemCount); + break; + } + if (mNewDataStart >= newSize) { + int itemCount = mOldDataSize - mOldDataStart; + mSize -= itemCount; + mCallback.onRemoved(mNewDataStart, itemCount); + break; + } + + T oldItem = mOldData[mOldDataStart]; + T newItem = newData[mNewDataStart]; + + int result = mCallback.compare(oldItem, newItem); + if (result < 0) { + replaceAllRemove(); + } else if (result > 0) { + replaceAllInsert(newItem); + } else { + if (!mCallback.areItemsTheSame(oldItem, newItem)) { + // The items aren't the same even though they were supposed to occupy the same + // place, so both notify to remove and add an item in the current location. + replaceAllRemove(); + replaceAllInsert(newItem); + } else { + mData[mNewDataStart] = newItem; + mOldDataStart++; + mNewDataStart++; + if (!mCallback.areContentsTheSame(oldItem, newItem)) { + // The item is the same but the contents have changed, so notify that an + // onChanged event has occurred. + mCallback.onChanged(mNewDataStart - 1, 1, + mCallback.getChangePayload(oldItem, newItem)); + } + } + } + } mOldData = null; @@ -216,17 +330,33 @@ public class SortedList<T> { } } + private void replaceAllInsert(T newItem) { + mData[mNewDataStart] = newItem; + mNewDataStart++; + mSize++; + mCallback.onInserted(mNewDataStart - 1, 1); + } + + private void replaceAllRemove() { + mSize--; + mOldDataStart++; + mCallback.onRemoved(mNewDataStart, 1); + } + /** - * Remove duplicate items, leaving only the last item from each group of "same" items. - * Move the remaining items to the beginning of the array. + * Sorts and removes duplicate items, leaving only the last item from each group of "same" + * items. Move the remaining items to the beginning of the array. * * @return Number of deduplicated items at the beginning of the array. */ - private int deduplicate(T[] items) { + private int sortAndDedup(@NonNull T[] items) { if (items.length == 0) { - throw new IllegalArgumentException("Input array must be non-empty"); + return 0; } + // Arrays.sort is stable. + Arrays.sort(items, mCallback); + // Keep track of the range of equal items at the end of the output. // Start with the range containing just the first item. int rangeStart = 0; @@ -236,9 +366,6 @@ public class SortedList<T> { T currentItem = items[i]; int compare = mCallback.compare(items[rangeStart], currentItem); - if (compare > 0) { - throw new IllegalArgumentException("Input must be sorted in ascending order."); - } if (compare == 0) { // The range of equal items continues, update it. @@ -278,27 +405,36 @@ public class SortedList<T> { * This method assumes that newItems are sorted and deduplicated. */ private void merge(T[] newData, int newDataSize) { + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + if (forceBatchedUpdates) { + beginBatchedUpdates(); + } + + mOldData = mData; + mOldDataStart = 0; + mOldDataSize = mSize; + final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; mData = (T[]) Array.newInstance(mTClass, mergedCapacity); - mMergedSize = 0; + mNewDataStart = 0; int newDataStart = 0; while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) { if (mOldDataStart == mOldDataSize) { // No more old items, copy the remaining new items. int itemCount = newDataSize - newDataStart; - System.arraycopy(newData, newDataStart, mData, mMergedSize, itemCount); - mMergedSize += itemCount; + System.arraycopy(newData, newDataStart, mData, mNewDataStart, itemCount); + mNewDataStart += itemCount; mSize += itemCount; - mCallback.onInserted(mMergedSize - itemCount, itemCount); + mCallback.onInserted(mNewDataStart - itemCount, itemCount); break; } if (newDataStart == newDataSize) { // No more new items, copy the remaining old items. int itemCount = mOldDataSize - mOldDataStart; - System.arraycopy(mOldData, mOldDataStart, mData, mMergedSize, itemCount); - mMergedSize += itemCount; + System.arraycopy(mOldData, mOldDataStart, mData, mNewDataStart, itemCount); + mNewDataStart += itemCount; break; } @@ -307,36 +443,47 @@ public class SortedList<T> { int compare = mCallback.compare(oldItem, newItem); if (compare > 0) { // New item is lower, output it. - mData[mMergedSize++] = newItem; + mData[mNewDataStart++] = newItem; mSize++; newDataStart++; - mCallback.onInserted(mMergedSize - 1, 1); + mCallback.onInserted(mNewDataStart - 1, 1); } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) { // Items are the same. Output the new item, but consume both. - mData[mMergedSize++] = newItem; + mData[mNewDataStart++] = newItem; newDataStart++; mOldDataStart++; if (!mCallback.areContentsTheSame(oldItem, newItem)) { - mCallback.onChanged(mMergedSize - 1, 1, + mCallback.onChanged(mNewDataStart - 1, 1, mCallback.getChangePayload(oldItem, newItem)); } } else { // Old item is lower than or equal to (but not the same as the new). Output it. // New item with the same sort order will be inserted later. - mData[mMergedSize++] = oldItem; + mData[mNewDataStart++] = oldItem; mOldDataStart++; } } + + mOldData = null; + + if (forceBatchedUpdates) { + endBatchedUpdates(); + } } - private void throwIfMerging() { + /** + * Throws an exception if called while we are in the middle of a mutation operation (addAll or + * replaceAll). + */ + private void throwIfInMutationOperation() { if (mOldData != null) { - throw new IllegalStateException("Cannot call this method from within addAll"); + throw new IllegalStateException("Data cannot be mutated in the middle of a batch " + + "update operation such as addAll or replaceAll."); } } /** - * Batches adapter updates that happen between calling this method until calling + * Batches adapter updates that happen after calling this method and before calling * {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop * and they are placed into consecutive indices, SortedList calls * {@link Callback#onInserted(int, int)} only once with the proper item count. If an event @@ -368,7 +515,7 @@ public class SortedList<T> { * has no effect. */ public void beginBatchedUpdates() { - throwIfMerging(); + throwIfInMutationOperation(); if (mCallback instanceof BatchedCallback) { return; } @@ -382,7 +529,7 @@ public class SortedList<T> { * Ends the update transaction and dispatches any remaining event to the callback. */ public void endBatchedUpdates() { - throwIfMerging(); + throwIfInMutationOperation(); if (mCallback instanceof BatchedCallback) { ((BatchedCallback) mCallback).dispatchLastEvent(); } @@ -424,7 +571,7 @@ public class SortedList<T> { * @return True if item is removed, false if item cannot be found in the list. */ public boolean remove(T item) { - throwIfMerging(); + throwIfInMutationOperation(); return remove(item, true); } @@ -436,7 +583,7 @@ public class SortedList<T> { * @return The removed item. */ public T removeItemAt(int index) { - throwIfMerging(); + throwIfInMutationOperation(); T item = get(index); removeItemAtIndex(index, true); return item; @@ -481,7 +628,7 @@ public class SortedList<T> { * @see #add(Object) */ public void updateItemAt(int index, T item) { - throwIfMerging(); + throwIfInMutationOperation(); final T existing = get(index); // assume changed if the same object is given back boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); @@ -535,7 +682,7 @@ public class SortedList<T> { * @see #add(Object) */ public void recalculatePositionOfItemAt(int index) { - throwIfMerging(); + throwIfInMutationOperation(); // TODO can be improved final T item = get(index); removeItemAtIndex(index, false); @@ -562,8 +709,8 @@ public class SortedList<T> { if (mOldData != null) { // The call is made from a callback during addAll execution. The data is split // between mData and mOldData. - if (index >= mMergedSize) { - return mOldData[index - mMergedSize + mOldDataStart]; + if (index >= mNewDataStart) { + return mOldData[index - mNewDataStart + mOldDataStart]; } } return mData[index]; @@ -579,13 +726,13 @@ public class SortedList<T> { */ public int indexOf(T item) { if (mOldData != null) { - int index = findIndexOf(item, mData, 0, mMergedSize, LOOKUP); + int index = findIndexOf(item, mData, 0, mNewDataStart, LOOKUP); if (index != INVALID_POSITION) { return index; } index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP); if (index != INVALID_POSITION) { - return index - mOldDataStart + mMergedSize; + return index - mOldDataStart + mNewDataStart; } return INVALID_POSITION; } @@ -662,11 +809,17 @@ public class SortedList<T> { mSize++; } + private T[] copyArray(T[] items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.length); + System.arraycopy(items, 0, copy, 0, items.length); + return copy; + } + /** * Removes all items from the SortedList. */ public void clear() { - throwIfMerging(); + throwIfInMutationOperation(); if (mSize == 0) { return; } @@ -722,8 +875,8 @@ public class SortedList<T> { * so * that you can change its behavior depending on your UI. * <p> - * For example, if you are using SortedList with a {@link android.support.v7.widget.RecyclerView.Adapter - * RecyclerView.Adapter}, you should + * For example, if you are using SortedList with a + * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should * return whether the items' visual representations are the same or not. * * @param oldItem The previous representation of the object. @@ -734,7 +887,7 @@ public class SortedList<T> { abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); /** - * Called by the SortedList to decide whether two object represent the same Item or not. + * Called by the SortedList to decide whether two objects represent the same Item or not. * <p> * For example, if your items have unique ids, this method should check their equality. * diff --git a/android/support/v7/util/SortedListTest.java b/android/support/v7/util/SortedListTest.java index 47d2ac0f..f8bc496c 100644 --- a/android/support/v7/util/SortedListTest.java +++ b/android/support/v7/util/SortedListTest.java @@ -27,11 +27,15 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; @RunWith(JUnit4.class) @SmallTest @@ -44,6 +48,8 @@ public class SortedListTest extends TestCase { List<Pair> mUpdates = new ArrayList<Pair>(); private boolean mPayloadChanges = false; List<PayloadChange> mPayloadUpdates = new ArrayList<>(); + Queue<AssertListStateRunnable> mCallbackRunnables; + List<Event> mEvents = new ArrayList<>(); private SortedList.Callback<Item> mCallback; InsertedCallback<Item> mInsertedCallback; ChangedCallback<Item> mChangedCallback; @@ -67,6 +73,7 @@ public class SortedListTest extends TestCase { @Before public void setUp() throws Exception { super.setUp(); + mCallback = new SortedList.Callback<Item>() { @Override public int compare(Item o1, Item o2) { @@ -75,28 +82,35 @@ public class SortedListTest extends TestCase { @Override public void onInserted(int position, int count) { + mEvents.add(new Event(TYPE.ADD, position, count)); mAdditions.add(new Pair(position, count)); if (mInsertedCallback != null) { mInsertedCallback.onInserted(position, count); } + pollAndRun(mCallbackRunnables); } @Override public void onRemoved(int position, int count) { + mEvents.add(new Event(TYPE.REMOVE, position, count)); mRemovals.add(new Pair(position, count)); + pollAndRun(mCallbackRunnables); } @Override public void onMoved(int fromPosition, int toPosition) { + mEvents.add(new Event(TYPE.MOVE, fromPosition, toPosition)); mMoves.add(new Pair(fromPosition, toPosition)); } @Override public void onChanged(int position, int count) { + mEvents.add(new Event(TYPE.CHANGE, position, count)); mUpdates.add(new Pair(position, count)); if (mChangedCallback != null) { mChangedCallback.onChanged(position, count); } + pollAndRun(mCallbackRunnables); } @Override @@ -110,7 +124,7 @@ public class SortedListTest extends TestCase { @Override public boolean areContentsTheSame(Item oldItem, Item newItem) { - return oldItem.cmpField == newItem.cmpField && oldItem.data == newItem.data; + return oldItem.data == newItem.data; } @Override @@ -127,11 +141,41 @@ public class SortedListTest extends TestCase { return null; } }; - mInsertedCallback = null; - mChangedCallback = null; mList = new SortedList<Item>(Item.class, mCallback); } + private void pollAndRun(Queue<AssertListStateRunnable> queue) { + if (queue != null) { + Runnable runnable = queue.poll(); + assertNotNull(runnable); + runnable.run(); + } + } + + @Test + public void testValidMethodsDuringOnInsertedCallbackFromEmptyList() { + + final Item[] items = + new Item[] {new Item(0), new Item(1), new Item(2)}; + + final AtomicInteger atomicInteger = new AtomicInteger(0); + mInsertedCallback = new InsertedCallback<Item>() { + @Override + public void onInserted(int position, int count) { + for (int i = 0; i < count; i++) { + assertEquals(mList.get(i), items[i]); + assertEquals(mList.indexOf(items[i]), i); + atomicInteger.incrementAndGet(); + } + } + }; + + mList.add(items[0]); + mList.clear(); + mList.addAll(items, false); + assertEquals(4, atomicInteger.get()); + } + @Test public void testEmpty() { assertEquals("empty", mList.size(), 0); @@ -139,16 +183,16 @@ public class SortedListTest extends TestCase { @Test public void testAdd() { - Item item = new Item(); + Item item = new Item(1); assertEquals(insert(item), 0); assertEquals(size(), 1); assertTrue(mAdditions.contains(new Pair(0, 1))); - Item item2 = new Item(); + Item item2 = new Item(2); item2.cmpField = item.cmpField + 1; assertEquals(insert(item2), 1); assertEquals(size(), 2); assertTrue(mAdditions.contains(new Pair(1, 1))); - Item item3 = new Item(); + Item item3 = new Item(3); item3.cmpField = item.cmpField - 1; mAdditions.clear(); assertEquals(insert(item3), 0); @@ -158,9 +202,8 @@ public class SortedListTest extends TestCase { @Test public void testAddDuplicate() { - Item item = new Item(); - Item item2 = new Item(item.id, item.cmpField); - item2.data = item.data; + Item item = new Item(1); + Item item2 = new Item(item.id); insert(item); assertEquals(0, insert(item2)); assertEquals(1, size()); @@ -170,7 +213,7 @@ public class SortedListTest extends TestCase { @Test public void testRemove() { - Item item = new Item(); + Item item = new Item(1); assertFalse(remove(item)); assertEquals(0, mRemovals.size()); insert(item); @@ -184,8 +227,8 @@ public class SortedListTest extends TestCase { @Test public void testRemove2() { - Item item = new Item(); - Item item2 = new Item(item.cmpField); + Item item = new Item(1); + Item item2 = new Item(2, 1, 1); insert(item); assertFalse(remove(item2)); assertEquals(0, mRemovals.size()); @@ -218,11 +261,12 @@ public class SortedListTest extends TestCase { Random random = new Random(System.nanoTime()); List<Item> copy = new ArrayList<Item>(); StringBuilder log = new StringBuilder(); + int id = 1; try { for (int i = 0; i < 10000; i++) { switch (random.nextInt(3)) { case 0://ADD - Item item = new Item(); + Item item = new Item(id++); copy.add(item); insert(item); log.append("add ").append(item).append("\n"); @@ -241,12 +285,13 @@ public class SortedListTest extends TestCase { int index = random.nextInt(mList.size()); item = mList.get(index); // TODO this cannot work - Item newItem = new Item(item.id, item.cmpField); - log.append("update ").append(item).append(" to ").append(newItem) - .append("\n"); + Item newItem = + new Item(item.id, item.cmpField, random.nextInt(1000)); while (newItem.data == item.data) { newItem.data = random.nextInt(1000); } + log.append("update ").append(item).append(" to ").append(newItem) + .append("\n"); int itemIndex = mList.add(newItem); copy.remove(item); copy.add(newItem); @@ -258,10 +303,12 @@ public class SortedListTest extends TestCase { if (copy.size() > 0) { int index = random.nextInt(mList.size()); item = mList.get(index); - Item newItem = new Item(item.id, random.nextInt()); + Item newItem = new Item(item.id, random.nextInt(), random.nextInt()); mList.updateItemAt(index, newItem); copy.remove(item); copy.add(newItem); + log.append("update at ").append(index).append(" ").append(item) + .append(" to ").append(newItem).append("\n"); } } int lastCmp = Integer.MIN_VALUE; @@ -299,14 +346,21 @@ public class SortedListTest extends TestCase { Item[] items = new Item[count]; int id = idFrom; for (int i = 0; i < count; i++) { - Item item = new Item(id, id); - item.data = id; + Item item = new Item(id); items[i] = item; id += idStep; } return items; } + private static Item[] createItemsFromInts(int ... ints) { + Item[] items = new Item[ints.length]; + for (int i = ints.length - 1; i >= 0; i--) { + items[i] = new Item(ints[i]); + } + return items; + } + private static Item[] shuffle(Item[] items) { Random random = new Random(System.nanoTime()); final int count = items.length; @@ -493,8 +547,7 @@ public class SortedListTest extends TestCase { int uniqueId = 0; for (int cmpField = 0; cmpField < maxCmpField; cmpField++) { for (int id = 0; id < idsPerCmpField; id++) { - Item item = new Item(uniqueId++, cmpField); - item.data = generation; + Item item = new Item(uniqueId++, cmpField, generation); items[index++] = item; } } @@ -548,13 +601,13 @@ public class SortedListTest extends TestCase { @Test public void testAddAllStableSort() { int id = 0; - Item item = new Item(id++, 0); + Item item = new Item(id++, 0, 0); mList.add(item); // Create a few items with the same sort order. Item[] items = new Item[3]; for (int i = 0; i < 3; i++) { - items[i] = new Item(id++, item.cmpField); + items[i] = new Item(id++, item.cmpField, 0); assertEquals(0, mCallback.compare(item, items[i])); } @@ -576,6 +629,7 @@ public class SortedListTest extends TestCase { item.data = 1; } + mInsertedCallback = new InsertedCallback<Item>() { @Override public void onInserted(int position, int count) { @@ -585,6 +639,7 @@ public class SortedListTest extends TestCase { assertEquals(i * 2, mList.get(i).id); } assertIntegrity(5, "onInserted(" + position + ", " + count + ")"); + } }; @@ -639,7 +694,7 @@ public class SortedListTest extends TestCase { @Override public void onInserted(int position, int count) { try { - mList.add(new Item()); + mList.add(new Item(1)); fail("add must throw from within a callback"); } catch (IllegalStateException e) { } @@ -729,14 +784,14 @@ public class SortedListTest extends TestCase { @Test public void testAddExistingItemCallsChangeWithPayload() { mList.addAll( - new Item(1, 10), - new Item(2, 20), - new Item(3, 30) + new Item(1), + new Item(2), + new Item(3) ); 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); + final Item twoUpdate = new Item(2); twoUpdate.data = 1337; mList.add(twoUpdate); assertEquals(1, mPayloadUpdates.size()); @@ -750,14 +805,14 @@ public class SortedListTest extends TestCase { @Test public void testUpdateItemCallsChangeWithPayload() { mList.addAll( - new Item(1, 10), - new Item(2, 20), - new Item(3, 30) + new Item(1), + new Item(2), + new Item(3) ); 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); + final Item twoUpdate = new Item(2); twoUpdate.data = 1337; mList.updateItemAt(1, twoUpdate); assertEquals(1, mPayloadUpdates.size()); @@ -772,16 +827,16 @@ public class SortedListTest extends TestCase { @Test public void testAddMultipleExistingItemCallsChangeWithPayload() { mList.addAll( - new Item(1, 10), - new Item(2, 20), - new Item(3, 30) + new Item(1), + new Item(2), + new Item(3) ); 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); + final Item twoUpdate = new Item(2); twoUpdate.data = 222; - final Item threeUpdate = new Item(3, 30); + final Item threeUpdate = new Item(3); threeUpdate.data = 333; mList.addAll(twoUpdate, threeUpdate); assertEquals(2, mPayloadUpdates.size()); @@ -796,6 +851,648 @@ public class SortedListTest extends TestCase { assertEquals(3, size()); } + @Test + public void replaceAll_mayModifyInputFalse_doesNotModify() { + mList.addAll( + new Item(1), + new Item(2) + ); + Item replacement0 = new Item(4); + Item replacement1 = new Item(3); + Item[] replacements = new Item[]{ + replacement0, + replacement1 + }; + + mList.replaceAll(replacements, false); + + assertSame(replacement0, replacements[0]); + assertSame(replacement1, replacements[1]); + } + + @Test + public void replaceAll_varArgs_isEquivalentToDefault() { + mList.addAll( + new Item(1), + new Item(2) + ); + Item replacement0 = new Item(3); + Item replacement1 = new Item(4); + + mList.replaceAll(replacement0, replacement1); + + assertEquals(mList.get(0), replacement0); + assertEquals(mList.get(1), replacement1); + assertEquals(2, mList.size()); + } + + @Test + public void replaceAll_collection_isEquivalentToDefaultWithMayModifyInputFalse() { + mList.addAll( + new Item(1), + new Item(2) + ); + Item replacement0 = new Item(4); + Item replacement1 = new Item(3); + List<Item> replacements = new ArrayList<>(); + replacements.add(replacement0); + replacements.add(replacement1); + + mList.replaceAll(replacements); + + assertEquals(mList.get(0), replacement1); + assertEquals(mList.get(1), replacement0); + assertSame(replacements.get(0), replacement0); + assertSame(replacements.get(1), replacement1); + assertEquals(2, mList.size()); + } + + @Test + public void replaceAll_callsChangeWithPayload() { + mList.addAll( + new Item(1), + new Item(2), + new Item(3) + ); + mPayloadChanges = true; + final Item twoUpdate = new Item(2); + twoUpdate.data = 222; + final Item threeUpdate = new Item(3); + threeUpdate.data = 333; + + mList.replaceAll(twoUpdate, threeUpdate); + + assertEquals(2, mPayloadUpdates.size()); + final PayloadChange update1 = mPayloadUpdates.get(0); + assertEquals(0, update1.position); + assertEquals(1, update1.count); + assertEquals(222, update1.payload); + final PayloadChange update2 = mPayloadUpdates.get(1); + assertEquals(1, update2.position); + assertEquals(1, update2.count); + assertEquals(333, update2.payload); + } + + @Test + public void replaceAll_totallyEquivalentData_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2, 3); + Item[] items2 = createItemsFromInts(1, 2, 3); + mList.addAll(items1); + mEvents.clear(); + + mList.replaceAll(items2); + + assertEquals(0, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + } + + @Test + public void replaceAll_removalsAndAdds1_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 3, 5); + Item[] items2 = createItemsFromInts(2, 4); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(2, 3, 5))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(2, 5))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(2, 4, 5))); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(3)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(4)); + assertEquals(5, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_removalsAndAdds2_worksCorrectly() { + Item[] items1 = createItemsFromInts(2, 4); + Item[] items2 = createItemsFromInts(1, 3, 5); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 4))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 3, 4))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 3))); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(3)); + assertEquals(new Event(TYPE.ADD, 2, 1), mEvents.get(4)); + assertEquals(5, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_removalsAndAdds3_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 3, 5); + Item[] items2 = createItemsFromInts(2, 3, 4); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(2, 3, 5))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(2, 3, 4, 5))); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.ADD, 2, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.REMOVE, 3, 1), mEvents.get(3)); + assertEquals(4, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_removalsAndAdds4_worksCorrectly() { + Item[] items1 = createItemsFromInts(2, 3, 4); + Item[] items2 = createItemsFromInts(1, 3, 5); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 3, 4))); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 3))); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.ADD, 2, 1), mEvents.get(3)); + assertEquals(4, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_removalsAndAdds5_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2, 3); + Item[] items2 = createItemsFromInts(3, 4, 5); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 2), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 1, 2), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_removalsAndAdds6_worksCorrectly() { + Item[] items1 = createItemsFromInts(3, 4, 5); + Item[] items2 = createItemsFromInts(1, 2, 3); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 2), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 3, 2), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_move1_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2, 3); + Item[] items2 = new Item[]{ + new Item(2), + new Item(3), + new Item(1, 4, 1)}; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 2, 1), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_move2_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2, 3); + Item[] items2 = new Item[]{ + new Item(3, 0, 3), + new Item(1), + new Item(2)}; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 3, 1), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_move3_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 3, 5, 7, 9); + Item[] items2 = new Item[]{ + new Item(3, 0, 3), + new Item(1), + new Item(5), + new Item(9), + new Item(7, 10, 7), + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(3, 0, 3), + new Item(1), + new Item(5), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(3, 0, 3), + new Item(1), + new Item(5), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.REMOVE, 3, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.ADD, 4, 1), mEvents.get(3)); + assertEquals(4, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_move4_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 3, 5, 7, 9); + Item[] items2 = new Item[]{ + new Item(3), + new Item(1, 4, 1), + new Item(5), + new Item(9, 6, 9), + new Item(7), + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(3), + new Item(1, 4, 1), + new Item(5), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(3), + new Item(1, 4, 1), + new Item(5), + new Item(9, 6, 9), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.ADD, 3, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.REMOVE, 5, 1), mEvents.get(3)); + assertEquals(4, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_move5_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 3, 5, 7, 9); + Item[] items2 = new Item[]{ + new Item(9, 1, 9), + new Item(7, 3, 7), + new Item(5), + new Item(3, 7, 3), + new Item(1, 9, 1), + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(3), + new Item(5), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(5), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(7, 3, 7), + new Item(5), + new Item(7), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(7, 3, 7), + new Item(5), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(7, 3, 7), + new Item(5), + new Item(3, 7, 3), + new Item(9) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(9, 1, 9), + new Item(7, 3, 7), + new Item(5), + new Item(3, 7, 3) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(3)); + assertEquals(new Event(TYPE.REMOVE, 3, 1), mEvents.get(4)); + assertEquals(new Event(TYPE.ADD, 3, 1), mEvents.get(5)); + assertEquals(new Event(TYPE.REMOVE, 4, 1), mEvents.get(6)); + assertEquals(new Event(TYPE.ADD, 4, 1), mEvents.get(7)); + assertEquals(8, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_orderSameItemDifferent_worksCorrectly() { + Item[] items1 = new Item[]{ + new Item(1), + new Item(2, 3, 2), + new Item(5) + }; + Item[] items2 = new Item[]{ + new Item(1), + new Item(4, 3, 4), + new Item(5) + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_orderSameItemSameContentsDifferent_worksCorrectly() { + Item[] items1 = new Item[]{ + new Item(1), + new Item(3, 3, 2), + new Item(5) + }; + Item[] items2 = new Item[]{ + new Item(1), + new Item(3, 3, 4), + new Item(5) + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.CHANGE, 1, 1), mEvents.get(0)); + assertEquals(1, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_allTypesOfChanges1_worksCorrectly() { + Item[] items1 = createItemsFromInts(2, 5, 6); + Item[] items2 = new Item[]{ + new Item(1), + new Item(3, 2, 3), + new Item(6, 6, 7) + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(createItemsFromInts(1, 5, 6))); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(1), + new Item(3, 2, 3), + new Item(5), + new Item(6) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(1), + new Item(3, 2, 3), + new Item(6) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.ADD, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.REMOVE, 1, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(3)); + assertEquals(new Event(TYPE.CHANGE, 2, 1), mEvents.get(4)); + assertEquals(5, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_allTypesOfChanges2_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 4, 6); + Item[] items2 = new Item[]{ + new Item(1, 1, 2), + new Item(3), + new Item(5, 4, 5) + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(1, 1, 2), + new Item(3), + new Item(4), + new Item(6) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(1, 1, 2), + new Item(3), + new Item(6) + )); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(1, 1, 2), + new Item(3), + new Item(5, 4, 5), + new Item(6) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.CHANGE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 1, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.REMOVE, 2, 1), mEvents.get(2)); + assertEquals(new Event(TYPE.ADD, 2, 1), mEvents.get(3)); + assertEquals(new Event(TYPE.REMOVE, 3, 1), mEvents.get(4)); + assertEquals(5, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_allTypesOfChanges3_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2); + Item[] items2 = new Item[]{ + new Item(2, 2, 3), + new Item(3, 2, 4), + new Item(5) + }; + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable( + new Item(2, 2, 3) + )); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.replaceAll(items2); + + assertEquals(new Event(TYPE.REMOVE, 0, 1), mEvents.get(0)); + assertEquals(new Event(TYPE.CHANGE, 0, 1), mEvents.get(1)); + assertEquals(new Event(TYPE.ADD, 1, 2), mEvents.get(2)); + assertEquals(3, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + + @Test + public void replaceAll_newItemsAreIdentical_resultIsDeduped() { + Item[] items = createItemsFromInts(1, 1); + mList.replaceAll(items); + + assertEquals(new Item(1), mList.get(0)); + assertEquals(1, mList.size()); + } + + @Test + public void replaceAll_newItemsUnsorted_resultIsSorted() { + Item[] items = createItemsFromInts(2, 1); + mList.replaceAll(items); + + assertEquals(new Item(1), mList.get(0)); + assertEquals(new Item(2), mList.get(1)); + assertEquals(2, mList.size()); + } + + @Test + public void replaceAll_calledAfterBeginBatchedUpdates_worksCorrectly() { + Item[] items1 = createItemsFromInts(1, 2, 3); + Item[] items2 = createItemsFromInts(4, 5, 6); + mList.addAll(items1); + mEvents.clear(); + + mCallbackRunnables = new LinkedList<>(); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + mCallbackRunnables.add(new AssertListStateRunnable(items2)); + + mList.beginBatchedUpdates(); + mList.replaceAll(items2); + mList.endBatchedUpdates(); + + assertEquals(new Event(TYPE.REMOVE, 0, 3), mEvents.get(0)); + assertEquals(new Event(TYPE.ADD, 0, 3), mEvents.get(1)); + assertEquals(2, mEvents.size()); + assertTrue(sortedListEquals(mList, items2)); + assertTrue(mCallbackRunnables.isEmpty()); + } + private int size() { return mList.size(); } @@ -810,63 +1507,33 @@ public class SortedListTest extends TestCase { static class Item { - static int idCounter = 0; final int id; - int cmpField; + int data; - int data = (int) (Math.random() * 1000);//used for comparison - - public Item() { - id = idCounter++; - cmpField = (int) (Math.random() * 1000); + Item(int allFields) { + this(allFields, allFields, allFields); } - public Item(int cmpField) { - id = idCounter++; - this.cmpField = cmpField; - } - - public Item(int id, int cmpField) { + Item(int id, int compField, int data) { this.id = id; - this.cmpField = cmpField; + this.cmpField = compField; + this.data = data; } @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; Item item = (Item) o; - if (cmpField != item.cmpField) { - return false; - } - if (id != item.id) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = id; - result = 31 * result + cmpField; - return result; + return id == item.id && cmpField == item.cmpField && data == item.data; } @Override public String toString() { - return "Item{" + - "id=" + id + - ", cmpField=" + cmpField + - ", data=" + data + - '}'; + return "Item(id=" + id + ", cmpField=" + cmpField + ", data=" + data + ')'; } } @@ -913,6 +1580,85 @@ public class SortedListTest extends TestCase { } } + private enum TYPE { + ADD, REMOVE, MOVE, CHANGE + } + + private final class AssertListStateRunnable implements Runnable { + + private Item[] mExpectedItems; + + AssertListStateRunnable(Item... expectedItems) { + this.mExpectedItems = expectedItems; + } + + @Override + public void run() { + try { + assertEquals(mExpectedItems.length, mList.size()); + for (int i = mExpectedItems.length - 1; i >= 0; i--) { + assertEquals(mExpectedItems[i], mList.get(i)); + assertEquals(i, mList.indexOf(mExpectedItems[i])); + } + } catch (AssertionError assertionError) { + throw new AssertionError( + assertionError.getMessage() + + "\nExpected: " + + Arrays.toString(mExpectedItems) + + "\nActual: " + + sortedListToString(mList)); + } + } + } + + private static final class Event { + private final TYPE mType; + private final int mVal1; + private final int mVal2; + + Event(TYPE type, int val1, int val2) { + this.mType = type; + this.mVal1 = val1; + this.mVal2 = val2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Event that = (Event) o; + return mType == that.mType && mVal1 == that.mVal1 && mVal2 == that.mVal2; + } + + @Override + public String toString() { + return "Event(" + mType + ", " + mVal1 + ", " + mVal2 + ")"; + } + } + + private <T> boolean sortedListEquals(SortedList<T> sortedList, T[] array) { + if (sortedList.size() != array.length) { + return false; + } + for (int i = sortedList.size() - 1; i >= 0; i--) { + if (!sortedList.get(i).equals(array[i])) { + return false; + } + } + return true; + } + + private static String sortedListToString(SortedList sortedList) { + StringBuilder stringBuilder = new StringBuilder("["); + int size = sortedList.size(); + for (int i = 0; i < size; i++) { + stringBuilder.append(sortedList.get(i).toString() + ", "); + } + stringBuilder.delete(stringBuilder.length() - 2, stringBuilder.length()); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + private static final class PayloadChange { public final int position; public final int count; diff --git a/android/support/v7/view/ContextThemeWrapper.java b/android/support/v7/view/ContextThemeWrapper.java index aa5b36e9..cc634804 100644 --- a/android/support/v7/view/ContextThemeWrapper.java +++ b/android/support/v7/view/ContextThemeWrapper.java @@ -16,26 +16,19 @@ package android.support.v7.view; -import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; - import android.content.Context; import android.content.ContextWrapper; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; -import android.support.annotation.RestrictTo; import android.support.annotation.StyleRes; import android.support.v7.appcompat.R; import android.view.LayoutInflater; /** - * A ContextWrapper that allows you to modify the theme from what is in the - * wrapped context. - * - * @hide + * A context wrapper that allows you to modify or replace the theme of the wrapped context. */ -@RestrictTo(LIBRARY_GROUP) public class ContextThemeWrapper extends ContextWrapper { private int mThemeResource; private Resources.Theme mTheme; @@ -110,15 +103,6 @@ public class ContextThemeWrapper extends ContextWrapper { mOverrideConfiguration = new Configuration(overrideConfiguration); } - /** - * Used by ActivityThread to apply the overridden configuration to onConfigurationChange - * callbacks. - * @hide - */ - public Configuration getOverrideConfiguration() { - return mOverrideConfiguration; - } - @Override public Resources getResources() { return getResourcesInternal(); @@ -144,6 +128,10 @@ public class ContextThemeWrapper extends ContextWrapper { } } + /** + * Returns the resource ID of the theme that is to be applied on top of the base context's + * theme. + */ public int getThemeResId() { return mThemeResource; } diff --git a/android/support/v7/widget/AppCompatAutoCompleteTextView.java b/android/support/v7/widget/AppCompatAutoCompleteTextView.java index 5b0a2f8d..e41bec75 100644 --- a/android/support/v7/widget/AppCompatAutoCompleteTextView.java +++ b/android/support/v7/widget/AppCompatAutoCompleteTextView.java @@ -29,6 +29,8 @@ import android.support.v4.view.TintableBackgroundView; import android.support.v7.appcompat.R; import android.support.v7.content.res.AppCompatResources; import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.AutoCompleteTextView; /** @@ -177,4 +179,10 @@ public class AppCompatAutoCompleteTextView extends AutoCompleteTextView implemen mTextHelper.onSetTextAppearance(context, resId); } } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs), + outAttrs, this); + } } diff --git a/android/support/v7/widget/AppCompatCheckedTextView.java b/android/support/v7/widget/AppCompatCheckedTextView.java index 921f0a26..dca409c9 100644 --- a/android/support/v7/widget/AppCompatCheckedTextView.java +++ b/android/support/v7/widget/AppCompatCheckedTextView.java @@ -20,6 +20,8 @@ import android.content.Context; import android.support.annotation.DrawableRes; import android.support.v7.content.res.AppCompatResources; import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.CheckedTextView; /** @@ -79,4 +81,10 @@ public class AppCompatCheckedTextView extends CheckedTextView { mTextHelper.applyCompoundDrawablesTints(); } } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs), + outAttrs, this); + } } diff --git a/android/support/v7/widget/AppCompatEditText.java b/android/support/v7/widget/AppCompatEditText.java index 406e364e..6831fcbf 100644 --- a/android/support/v7/widget/AppCompatEditText.java +++ b/android/support/v7/widget/AppCompatEditText.java @@ -28,6 +28,8 @@ import android.support.annotation.RestrictTo; import android.support.v4.view.TintableBackgroundView; import android.support.v7.appcompat.R; import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.EditText; /** @@ -159,4 +161,10 @@ public class AppCompatEditText extends EditText implements TintableBackgroundVie mTextHelper.onSetTextAppearance(context, resId); } } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs), + outAttrs, this); + } } diff --git a/android/support/v7/widget/AppCompatHintHelper.java b/android/support/v7/widget/AppCompatHintHelper.java new file mode 100644 index 00000000..0d30fb7c --- /dev/null +++ b/android/support/v7/widget/AppCompatHintHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.view.View; +import android.view.ViewParent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +class AppCompatHintHelper { + + static InputConnection onCreateInputConnection(InputConnection ic, EditorInfo outAttrs, + View view) { + if (ic != null && outAttrs.hintText == null) { + // If we don't have a hint and the parent implements WithHint, use its hint for the + // EditorInfo. This allows us to display a hint in 'extract mode'. + ViewParent parent = view.getParent(); + while (parent instanceof View) { + if (parent instanceof WithHint) { + outAttrs.hintText = ((WithHint) parent).getHint(); + break; + } + parent = parent.getParent(); + } + } + return ic; + } + +} diff --git a/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java b/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java index 8060d7d1..b71b08a5 100644 --- a/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java +++ b/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java @@ -29,6 +29,8 @@ import android.support.v4.view.TintableBackgroundView; import android.support.v7.appcompat.R; import android.support.v7.content.res.AppCompatResources; import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.MultiAutoCompleteTextView; /** @@ -177,4 +179,10 @@ public class AppCompatMultiAutoCompleteTextView extends MultiAutoCompleteTextVie mTextHelper.onSetTextAppearance(context, resId); } } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs), + outAttrs, this); + } } diff --git a/android/support/v7/widget/AppCompatTextView.java b/android/support/v7/widget/AppCompatTextView.java index cfa6a2a9..d8132770 100644 --- a/android/support/v7/widget/AppCompatTextView.java +++ b/android/support/v7/widget/AppCompatTextView.java @@ -31,6 +31,8 @@ import android.support.v4.widget.AutoSizeableTextView; import android.support.v4.widget.TextViewCompat; import android.support.v7.appcompat.R; import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.TextView; /** @@ -361,4 +363,10 @@ public class AppCompatTextView extends TextView implements TintableBackgroundVie } return new int[0]; } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return AppCompatHintHelper.onCreateInputConnection(super.onCreateInputConnection(outAttrs), + outAttrs, this); + } } diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java index 4bc17a86..84c28b10 100644 --- a/android/support/v7/widget/RecyclerView.java +++ b/android/support/v7/widget/RecyclerView.java @@ -386,8 +386,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners; /** - * Set to true when an adapter data set changed notification is received. - * In that case, we cannot run any animations since we don't know what happened until layout. + * True after an event occurs that signals that the entire data set has changed. In that case, + * we cannot run any animations since we don't know what happened until layout. * * Attached items are invalid until next layout, at which point layout will animate/replace * items as necessary, building up content from the (effectively) new adapter from scratch. @@ -395,11 +395,20 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * Cached items must be discarded when setting this to true, so that the cache may be freely * used by prefetching until the next layout occurs. * - * @see #setDataSetChangedAfterLayout() + * @see #processDataSetCompletelyChanged(boolean) */ boolean mDataSetHasChangedAfterLayout = false; /** + * True after the data set has completely changed and + * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent + * measure/layout. + * + * @see #processDataSetCompletelyChanged(boolean) + */ + boolean mDispatchItemsChangedEvent = false; + + /** * This variable is incremented during a dispatchLayout and/or scroll. * Some methods should not be called during these periods (e.g. adapter data change). * Doing so will create hard to find bugs so we better check it and throw an exception. @@ -1044,6 +1053,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, true, removeAndRecycleExistingViews); + processDataSetCompletelyChanged(true); requestLayout(); } /** @@ -1059,6 +1069,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); + processDataSetCompletelyChanged(false); requestLayout(); } @@ -1112,7 +1123,6 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious); mState.mStructureChanged = true; - setDataSetChangedAfterLayout(); } /** @@ -2509,9 +2519,17 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (next == null || next == this) { return false; } + // panic, result view is not a child anymore, maybe workaround b/37864393 + if (findContainingItemView(next) == null) { + return false; + } if (focused == null) { return true; } + // panic, focused view is not a child anymore, maybe workaround b/37864393 + if (findContainingItemView(focused) == null) { + return true; + } mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); mTempRect2.set(0, 0, next.getWidth(), next.getHeight()); @@ -3221,7 +3239,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Used when onMeasure is called before layout manager is set + * An implementation of {@link View#onMeasure(int, int)} to fall back to in various scenarios + * where this RecyclerView is otherwise lacking better information. */ void defaultOnMeasure(int widthSpec, int heightSpec) { // calling LayoutManager here is not pretty but that API is already public and it is better @@ -3398,7 +3417,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // Processing these items have no value since data set changed unexpectedly. // Instead, we just reset it. mAdapterHelper.reset(); - mLayout.onItemsChanged(this); + if (mDispatchItemsChangedEvent) { + mLayout.onItemsChanged(this); + } } // simple animations are a subset of advanced animations (which will cause a // pre-layout step) @@ -3821,6 +3842,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mLayout.removeAndRecycleScrapInt(mRecycler); mState.mPreviousLayoutItemCount = mState.mItemCount; mDataSetHasChangedAfterLayout = false; + mDispatchItemsChangedEvent = false; mState.mRunSimpleAnimations = false; mState.mRunPredictiveAnimations = false; @@ -4288,19 +4310,21 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro viewHolder.getUnmodifiedPayloads()); } - /** - * Call this method to signal that *all* adapter content has changed (generally, because of - * setAdapter, swapAdapter, or notifyDataSetChanged), and that once layout occurs, all - * attached items should be discarded or animated. + * Processes the fact that, as far as we can tell, the data set has completely changed. * - * Attached items are labeled as invalid, and all cached items are discarded. + * <ul> + * <li>Once layout occurs, all attached items should be discarded or animated. + * <li>Attached items are labeled as invalid. + * <li>Because items may still be prefetched between a "data set completely changed" + * event and a layout event, all cached items are discarded. + * </ul> * - * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true, - * so this method must always discard all cached views so that the only valid items that remain - * in the cache, once layout occurs, are valid prefetched items. + * @param dispatchItemsChanged Whether to call + * {@link LayoutManager#onItemsChanged(RecyclerView)} during measure/layout. */ - void setDataSetChangedAfterLayout() { + void processDataSetCompletelyChanged(boolean dispatchItemsChanged) { + mDispatchItemsChangedEvent |= dispatchItemsChanged; mDataSetHasChangedAfterLayout = true; markKnownViewsInvalid(); } @@ -5110,7 +5134,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro assertNotInLayoutOrScroll(null); mState.mStructureChanged = true; - setDataSetChangedAfterLayout(); + processDataSetCompletelyChanged(true); if (!mAdapterHelper.hasPendingUpdates()) { requestLayout(); } @@ -7419,9 +7443,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * wants to handle the layout measurements itself. * <p> * This method is usually called by the LayoutManager with value {@code true} if it wants - * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize - * the measurement logic, you can call this method with {@code false} and override - * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic. + * to support {@link ViewGroup.LayoutParams#WRAP_CONTENT}. If you are using a public + * LayoutManager but want to customize the measurement logic, you can call this method with + * {@code false} and override {@link LayoutManager#onMeasure(Recycler, State, int, int)} to + * implement your custom measurement logic. * <p> * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or * handle various specs provided by the RecyclerView's parent. @@ -7495,24 +7520,26 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Returns whether this LayoutManager supports automatic item animations. - * A LayoutManager wishing to support item animations should obey certain - * rules as outlined in {@link #onLayoutChildren(Recycler, State)}. - * The default return value is <code>false</code>, so subclasses of LayoutManager - * will not get predictive item animations by default. - * - * <p>Whether item animations are enabled in a RecyclerView is determined both - * by the return value from this method and the + * Returns whether this LayoutManager supports "predictive item animations". + * <p> + * "Predictive item animations" are automatically created animations that show + * where items came from, and where they are going to, as items are added, removed, + * or moved within a layout. + * <p> + * A LayoutManager wishing to support predictive item animations must override this + * method to return true (the default implementation returns false) and must obey certain + * behavioral contracts outlined in {@link #onLayoutChildren(Recycler, State)}. + * <p> + * Whether item animations actually occur in a RecyclerView is actually determined by both + * the return value from this method and the * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this - * method returns false, then simple item animations will be enabled, in which - * views that are moving onto or off of the screen are simply faded in/out. If - * the RecyclerView has a non-null ItemAnimator and this method returns true, - * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to - * setup up the information needed to more intelligently predict where appearing - * and disappearing views should be animated from/to.</p> + * method returns false, then only "simple item animations" will be enabled in the + * RecyclerView, in which views whose position are changing are simply faded in/out. If the + * RecyclerView has a non-null ItemAnimator and this method returns true, then predictive + * item animations will be enabled in the RecyclerView. * - * @return true if predictive item animations should be enabled, false otherwise + * @return true if this LayoutManager supports predictive item animations, false otherwise. */ public boolean supportsPredictiveItemAnimations() { return false; @@ -9491,9 +9518,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Called if the RecyclerView this LayoutManager is bound to has a different adapter set. - * The LayoutManager may use this opportunity to clear caches and configure state such - * that it can relayout appropriately with the new data and potentially new view types. + * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via + * {@link RecyclerView#setAdapter(Adapter)} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this + * opportunity to clear caches and configure state such that it can relayout appropriately + * with the new data and potentially new view types. * * <p>The default implementation removes all currently attached views.</p> * @@ -9535,8 +9564,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving - * detailed information on what has actually changed. + * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire + * data set has changed. * * @param recyclerView */ @@ -10042,7 +10072,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (vScroll == 0 && hScroll == 0) { return false; } - mRecyclerView.scrollBy(hScroll, vScroll); + mRecyclerView.smoothScrollBy(hScroll, vScroll); return true; } @@ -11794,6 +11824,11 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro boolean mStructureChanged = false; + /** + * True if the associated {@link RecyclerView} is in the pre-layout step where it is having + * its {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. + */ boolean mInPreLayout = false; boolean mTrackOldChangeHolders = false; @@ -11869,8 +11904,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } /** - * Returns true if - * @return + * Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its + * {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. */ public boolean isPreLayout() { return mInPreLayout; diff --git a/android/support/v7/widget/Toolbar.java b/android/support/v7/widget/Toolbar.java index 45e25830..f383e90c 100644 --- a/android/support/v7/widget/Toolbar.java +++ b/android/support/v7/widget/Toolbar.java @@ -56,6 +56,7 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -2366,12 +2367,20 @@ public class Toolbar extends ViewGroup { @Override public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { ensureCollapseButtonView(); - if (mCollapseButtonView.getParent() != Toolbar.this) { + ViewParent collapseButtonParent = mCollapseButtonView.getParent(); + if (collapseButtonParent != Toolbar.this) { + if (collapseButtonParent instanceof ViewGroup) { + ((ViewGroup) collapseButtonParent).removeView(mCollapseButtonView); + } addView(mCollapseButtonView); } mExpandedActionView = item.getActionView(); mCurrentExpandedItem = item; - if (mExpandedActionView.getParent() != Toolbar.this) { + ViewParent expandedActionParent = mExpandedActionView.getParent(); + if (expandedActionParent != Toolbar.this) { + if (expandedActionParent instanceof ViewGroup) { + ((ViewGroup) expandedActionParent).removeView(mExpandedActionView); + } final LayoutParams lp = generateDefaultLayoutParams(); lp.gravity = GravityCompat.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); lp.mViewType = LayoutParams.EXPANDED; diff --git a/android/support/v7/widget/TooltipPopup.java b/android/support/v7/widget/TooltipPopup.java index dc20aa1f..396fe058 100644 --- a/android/support/v7/widget/TooltipPopup.java +++ b/android/support/v7/widget/TooltipPopup.java @@ -31,6 +31,7 @@ import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.TextView; @@ -99,6 +100,7 @@ class TooltipPopup { private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch, WindowManager.LayoutParams outParams) { + outParams.token = anchorView.getApplicationWindowToken(); final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset( R.dimen.tooltip_precise_anchor_threshold); @@ -157,7 +159,7 @@ class TooltipPopup { mTmpAnchorPos[1] -= mTmpAppPos[1]; // mTmpAnchorPos is now relative to the main app window. - outParams.x = mTmpAnchorPos[0] + offsetX - mTmpDisplayFrame.width() / 2; + outParams.x = mTmpAnchorPos[0] + offsetX - appView.getWidth() / 2; final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); mContentView.measure(spec, spec); @@ -181,6 +183,16 @@ class TooltipPopup { } private static View getAppRootView(View anchorView) { + View rootView = anchorView.getRootView(); + ViewGroup.LayoutParams lp = rootView.getLayoutParams(); + if (lp instanceof WindowManager.LayoutParams + && (((WindowManager.LayoutParams) lp).type + == WindowManager.LayoutParams.TYPE_APPLICATION)) { + // This covers regular app windows and Dialog windows. + return rootView; + } + // For non-application window types (such as popup windows) try to find the main app window + // through the context. Context context = anchorView.getContext(); while (context instanceof ContextWrapper) { if (context instanceof Activity) { @@ -189,6 +201,8 @@ class TooltipPopup { context = ((ContextWrapper) context).getBaseContext(); } } - return anchorView.getRootView(); + // Main app window not found, fall back to the anchor's root view. There is no guarantee + // that the tooltip position will be computed correctly. + return rootView; } } diff --git a/android/support/v7/widget/WithHint.java b/android/support/v7/widget/WithHint.java new file mode 100644 index 00000000..d14f483a --- /dev/null +++ b/android/support/v7/widget/WithHint.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; + +/** + * @hide + */ +@RestrictTo(LIBRARY_GROUP) +public interface WithHint { + /** + * Returns the hint which is displayed in the floating label, if enabled. + * + * @return the hint, or null if there isn't one set, or the hint is not enabled. + */ + @Nullable + CharSequence getHint(); +} diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java index 5db93830..0077a5bd 100644 --- a/android/support/wear/ambient/AmbientMode.java +++ b/android/support/wear/ambient/AmbientMode.java @@ -21,7 +21,9 @@ import android.app.FragmentManager; import android.content.Context; import android.os.Bundle; import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.util.Log; import com.google.android.wearable.compat.WearableActivityController; @@ -48,6 +50,7 @@ import java.io.PrintWriter; * }</pre> */ public final class AmbientMode extends Fragment { + private static final String TAG = "AmbientMode"; /** * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate @@ -104,9 +107,6 @@ public final class AmbientMode extends Fragment { * running (after onResume, before onPause). All drawing should complete by the conclusion * of this method. Note that {@code invalidate()} calls will be executed before resuming * lower-power mode. - * <p> - * <p><em>Derived classes must call through to the super class's implementation of this - * method. If they do not, an exception will be thrown.</em> * * @param ambientDetails bundle containing information about the display being used. * It includes information about low-bit color and burn-in protection. @@ -122,9 +122,6 @@ public final class AmbientMode extends Fragment { /** * Called when an activity should exit ambient mode. This event is sent while an activity is * running (after onResume, before onPause). - * <p> - * <p><em>Derived classes must call through to the super class's implementation of this - * method. If they do not, an exception will be thrown.</em> */ public void onExitAmbient() {} } @@ -133,20 +130,27 @@ public final class AmbientMode extends Fragment { new AmbientDelegate.AmbientCallback() { @Override public void onEnterAmbient(Bundle ambientDetails) { - mSuppliedCallback.onEnterAmbient(ambientDetails); + if (mSuppliedCallback != null) { + mSuppliedCallback.onEnterAmbient(ambientDetails); + } } @Override public void onExitAmbient() { - mSuppliedCallback.onExitAmbient(); + if (mSuppliedCallback != null) { + mSuppliedCallback.onExitAmbient(); + } } @Override public void onUpdateAmbient() { - mSuppliedCallback.onUpdateAmbient(); + if (mSuppliedCallback != null) { + mSuppliedCallback.onUpdateAmbient(); + } } }; private AmbientDelegate mDelegate; + @Nullable private AmbientCallback mSuppliedCallback; private AmbientController mController; @@ -166,8 +170,7 @@ public final class AmbientMode extends Fragment { if (context instanceof AmbientCallbackProvider) { mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback(); } else { - throw new IllegalArgumentException( - "fragment should attach to an activity that implements AmbientCallback"); + Log.w(TAG, "No callback provided - enabling only smart resume"); } } @@ -176,7 +179,9 @@ public final class AmbientMode extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDelegate.onCreate(); - mDelegate.setAmbientEnabled(); + if (mSuppliedCallback != null) { + mDelegate.setAmbientEnabled(); + } } @Override @@ -215,15 +220,19 @@ public final class AmbientMode extends Fragment { } /** - * Attach ambient support to the given activity. + * Attach ambient support to the given activity. Calling this method with an Activity + * implementing the {@link AmbientCallbackProvider} interface will provide you with an + * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively, + * you can call this method with an Activity which does not implement + * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume + * functionality. This is equivalent to providing (@code null} from + * the {@link AmbientCallbackProvider}. * - * @param activity the activity to attach ambient support to. This activity has to also - * implement {@link AmbientCallbackProvider} + * @param activity the activity to attach ambient support to. * @return the associated {@link AmbientController} which can be used to query the state of * ambient mode. */ - public static <T extends Activity & AmbientCallbackProvider> AmbientController - attachAmbientSupport(T activity) { + public static <T extends Activity> AmbientController attachAmbientSupport(T activity) { FragmentManager fragmentManager = activity.getFragmentManager(); AmbientMode ambientFragment = (AmbientMode) fragmentManager.findFragmentByTag(FRAGMENT_TAG); if (ambientFragment == null) { |