Merge "Add empty source file to fix compose runtime tests runner." into androidx-master-dev
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
index 85e6f4a..88847fa 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
@@ -58,9 +58,9 @@
assertWithMessage("The parent graph should be resumed when its child is resumed")
.that(graphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val startBackStackEntry = navController.findBackStackEntry(R.id.start_test)
+ val startBackStackEntry = navController.getBackStackEntry(R.id.start_test)
assertWithMessage("The start destination should be resumed")
- .that(startBackStackEntry!!.lifecycle.currentState)
+ .that(startBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -71,9 +71,9 @@
assertWithMessage("The start destination should be set back to created after you navigate")
.that(startBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.popBackStack()
@@ -119,9 +119,9 @@
assertWithMessage("The parent graph should be resumed when its child is resumed")
.that(graphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val startBackStackEntry = navController.findBackStackEntry(R.id.start_test)
+ val startBackStackEntry = navController.getBackStackEntry(R.id.start_test)
assertWithMessage("The start destination should be resumed")
- .that(startBackStackEntry!!.lifecycle.currentState)
+ .that(startBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -132,9 +132,9 @@
assertWithMessage("The start destination should be started when a FloatingWindow is open")
.that(startBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.popBackStack()
@@ -186,9 +186,9 @@
assertWithMessage("The nested graph should be resumed when its child is resumed")
.that(nestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
assertWithMessage("The nested start destination should be resumed")
- .that(nestedBackStackEntry!!.lifecycle.currentState)
+ .that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -202,9 +202,9 @@
assertWithMessage("The nested start destination should be stopped after navigate")
.that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.popBackStack()
@@ -250,9 +250,9 @@
assertWithMessage("The nested graph should be resumed when its child is resumed")
.that(nestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
assertWithMessage("The nested start destination should be resumed")
- .that(nestedBackStackEntry!!.lifecycle.currentState)
+ .that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -267,9 +267,9 @@
"FloatingWindow is open")
.that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.popBackStack()
@@ -312,9 +312,9 @@
val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
val nestedGraphObserver = mock(LifecycleEventObserver::class.java)
nestedGraphBackStackEntry.lifecycle.addObserver(nestedGraphObserver)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
val nestedObserver = mock(LifecycleEventObserver::class.java)
- nestedBackStackEntry!!.lifecycle.addObserver(nestedObserver)
+ nestedBackStackEntry.lifecycle.addObserver(nestedObserver)
val inOrder = inOrder(graphObserver, nestedGraphObserver, nestedObserver)
inOrder.verify(graphObserver).onStateChanged(
graphBackStackEntry, Lifecycle.Event.ON_CREATE)
@@ -389,9 +389,9 @@
val nestedGraphBackStackEntry = navController.getBackStackEntry(R.id.nested)
val nestedGraphObserver = mock(LifecycleEventObserver::class.java)
nestedGraphBackStackEntry.lifecycle.addObserver(nestedGraphObserver)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
val nestedObserver = mock(LifecycleEventObserver::class.java)
- nestedBackStackEntry!!.lifecycle.addObserver(nestedObserver)
+ nestedBackStackEntry.lifecycle.addObserver(nestedObserver)
val inOrder = inOrder(graphObserver, nestedGraphObserver, nestedObserver)
inOrder.verify(graphObserver).onStateChanged(
graphBackStackEntry, Lifecycle.Event.ON_CREATE)
@@ -459,9 +459,9 @@
assertWithMessage("The nested graph should be resumed when its child is resumed")
.that(nestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
assertWithMessage("The nested start destination should be resumed")
- .that(nestedBackStackEntry!!.lifecycle.currentState)
+ .that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -475,9 +475,9 @@
assertWithMessage("The nested start destination should be stopped after navigate")
.that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
// Navigate to a new instance of the nested graph
@@ -499,9 +499,9 @@
assertWithMessage("The new nested graph should be resumed when its child is resumed")
.that(newNestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val newNestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
assertWithMessage("The new nested start destination should be resumed")
- .that(newNestedBackStackEntry!!.lifecycle.currentState)
+ .that(newNestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
}
@@ -534,9 +534,9 @@
assertWithMessage("The nested graph should be resumed when its child is resumed")
.that(nestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val nestedBackStackEntry = navController.findBackStackEntry(R.id.nested_test)
+ val nestedBackStackEntry = navController.getBackStackEntry(R.id.nested_test)
assertWithMessage("The nested start destination should be resumed")
- .that(nestedBackStackEntry!!.lifecycle.currentState)
+ .that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
navController.navigate(R.id.second_test)
@@ -550,9 +550,9 @@
assertWithMessage("The nested start destination should be stopped after navigate")
.that(nestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
- val secondBackStackEntry = navController.findBackStackEntry(R.id.second_test)
+ val secondBackStackEntry = navController.getBackStackEntry(R.id.second_test)
assertWithMessage("The new destination should be resumed")
- .that(secondBackStackEntry!!.lifecycle.currentState)
+ .that(secondBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
// Navigate to a new instance of the nested graph using a deep link to a dialog
@@ -574,9 +574,9 @@
assertWithMessage("The new nested graph should be resumed when its child is resumed")
.that(newNestedGraphBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- val newNestedBackStackEntry = navController.findBackStackEntry(R.id.nested_second_test)
+ val newNestedBackStackEntry = navController.getBackStackEntry(R.id.nested_second_test)
assertWithMessage("The new nested start destination should be resumed")
- .that(newNestedBackStackEntry!!.lifecycle.currentState)
+ .that(newNestedBackStackEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
}
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
index 5299844..ab84f8c 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
@@ -150,7 +150,7 @@
} catch (e: IllegalArgumentException) {
assertThat(e)
.hasMessageThat().contains(
- "No NavGraph with ID $navGraphId is on the NavController's back stack"
+ "No destination with ID $navGraphId is on the NavController's back stack"
)
}
}
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
index 8794055..4ab77ad 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.java
@@ -1139,34 +1139,22 @@
throw new IllegalStateException("You must call setViewModelStore() before calling "
+ "getViewModelStoreOwner().");
}
- return getBackStackEntry(navGraphId);
- }
-
- /**
- * Gets the {@link NavBackStackEntry} for a NavGraph.
- *
- * @param navGraphId ID of a NavGraph that exists on the back stack
- * @throws IllegalArgumentException if the NavGraph is not on the back stack
- */
- @NonNull
- public NavBackStackEntry getBackStackEntry(@IdRes int navGraphId) {
- NavBackStackEntry lastFromBackStack = findBackStackEntry(navGraphId);
- if (lastFromBackStack == null
- || !(lastFromBackStack.getDestination() instanceof NavGraph)) {
- throw new IllegalArgumentException("No NavGraph with ID " + navGraphId + " is on the "
- + "NavController's back stack");
+ NavBackStackEntry lastFromBackStack = getBackStackEntry(navGraphId);
+ if (!(lastFromBackStack.getDestination() instanceof NavGraph)) {
+ throw new IllegalArgumentException("No NavGraph with ID " + navGraphId
+ + " is on the NavController's back stack");
}
return lastFromBackStack;
}
/**
- * Find the topmost {@link NavBackStackEntry} for a destination id.
+ * Gets the topmost {@link NavBackStackEntry} for a destination id.
*
* @param destinationId ID of a destination that exists on the back stack
* @throws IllegalArgumentException if the destination is not on the back stack
*/
- @Nullable
- NavBackStackEntry findBackStackEntry(@IdRes int destinationId) {
+ @NonNull
+ public NavBackStackEntry getBackStackEntry(@IdRes int destinationId) {
NavBackStackEntry lastFromBackStack = null;
Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
while (iterator.hasNext()) {
@@ -1177,6 +1165,10 @@
break;
}
}
+ if (lastFromBackStack == null) {
+ throw new IllegalArgumentException("No destination with ID " + destinationId
+ + " is on the NavController's back stack");
+ }
return lastFromBackStack;
}
}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerExtraLayoutSpaceTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerExtraLayoutSpaceTest.java
index 70b1225..8f87cf5 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerExtraLayoutSpaceTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerExtraLayoutSpaceTest.java
@@ -69,6 +69,7 @@
private int mCurrPosition = 0;
private ScrollDirection mLastScrollDirection = TOWARDS_END;
+ private LastScrollDeltaTracker mLastScrollTracker = new LastScrollDeltaTracker();
public LinearLayoutManagerExtraLayoutSpaceTest(Config config, int extraLayoutSpaceLegacy,
int extraLayoutSpace) {
@@ -116,6 +117,7 @@
mLayoutManager = (ExtraLayoutSpaceLayoutManager) super.mLayoutManager;
mLayoutManager.mExtraLayoutSpaceLegacy = mExtraLayoutSpaceLegacy;
mLayoutManager.mExtraLayoutSpace = mExtraLayoutSpace;
+ mRecyclerView.addOnScrollListener(mLastScrollTracker);
// Verify start position
verifyStartPosition();
@@ -142,6 +144,13 @@
// Perform the scroll
scrollToPosition(mCurrPosition, smoothScroll);
+ int direction = Integer.signum(mCurrPosition - prevPosition);
+ if (smoothScroll) {
+ // TODO(b/139350295): fix the overshoot instead of detecting it
+ while (!isLastScrollDirectionCorrect(direction)) {
+ correctLastScrollDirection();
+ }
+ }
// Update expected results
// Alignment means the side of the viewport to which mCurrPosition is aligned
@@ -155,6 +164,25 @@
verify(getExpectedExtraSpace(smoothScroll), getAvailableSpace(alignment));
}
+ private boolean isLastScrollDirectionCorrect(int expectedDirection) {
+ int lastDirection = mLastScrollTracker.get(mConfig.mOrientation);
+ int reversedModifier = isReversed() ? -1 : 1;
+ return lastDirection * reversedModifier * expectedDirection >= 0;
+ }
+
+ private void correctLastScrollDirection() throws Throwable {
+ final int dx = Integer.signum(mLastScrollTracker.getX());
+ final int dy = Integer.signum(mLastScrollTracker.getY());
+
+ mLayoutManager.expectIdleState(1);
+ mRecyclerView.smoothScrollBy(dx, dy);
+ mLayoutManager.waitForSnap(10);
+
+ mLayoutManager.expectIdleState(1);
+ mRecyclerView.smoothScrollBy(-dx, -dy);
+ mLayoutManager.waitForSnap(10);
+ }
+
private void scrollToPosition(final int position, final boolean smoothScroll) throws Throwable {
mActivityRule.runOnUiThread(new Runnable() {
@Override
@@ -269,6 +297,29 @@
);
}
+
+ private class LastScrollDeltaTracker extends RecyclerView.OnScrollListener {
+ public final int[] mLastScrollDelta = new int[2];
+
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ mLastScrollDelta[0] = dx;
+ mLastScrollDelta[1] = dy;
+ }
+
+ public int getX() {
+ return mLastScrollDelta[0];
+ }
+
+ public int getY() {
+ return mLastScrollDelta[1];
+ }
+
+ public int get(int orientation) {
+ return mLastScrollDelta[orientation];
+ }
+ }
+
class ExtraLayoutSpaceLayoutManager extends WrappedLinearLayoutManager {
int mExtraLayoutSpaceLegacy = -1;
int[] mExtraLayoutSpace = null;
diff --git a/ui/ui-core/api/1.0.0-alpha01.txt b/ui/ui-core/api/1.0.0-alpha01.txt
index bac3029..0a4e798 100644
--- a/ui/ui-core/api/1.0.0-alpha01.txt
+++ b/ui/ui-core/api/1.0.0-alpha01.txt
@@ -432,6 +432,11 @@
method public androidx.ui.core.IntPx getWidth();
}
+ public enum LayoutDirection {
+ enum_constant public static final androidx.ui.core.LayoutDirection Ltr;
+ enum_constant public static final androidx.ui.core.LayoutDirection Rtl;
+ }
+
public enum PointerEventPass {
enum_constant public static final androidx.ui.core.PointerEventPass InitialDown;
enum_constant public static final androidx.ui.core.PointerEventPass PostDown;
diff --git a/ui/ui-core/api/current.txt b/ui/ui-core/api/current.txt
index bac3029..0a4e798 100644
--- a/ui/ui-core/api/current.txt
+++ b/ui/ui-core/api/current.txt
@@ -432,6 +432,11 @@
method public androidx.ui.core.IntPx getWidth();
}
+ public enum LayoutDirection {
+ enum_constant public static final androidx.ui.core.LayoutDirection Ltr;
+ enum_constant public static final androidx.ui.core.LayoutDirection Rtl;
+ }
+
public enum PointerEventPass {
enum_constant public static final androidx.ui.core.PointerEventPass InitialDown;
enum_constant public static final androidx.ui.core.PointerEventPass PostDown;
diff --git a/ui/ui-core/api/restricted_1.0.0-alpha01.txt b/ui/ui-core/api/restricted_1.0.0-alpha01.txt
index bac3029..0a4e798 100644
--- a/ui/ui-core/api/restricted_1.0.0-alpha01.txt
+++ b/ui/ui-core/api/restricted_1.0.0-alpha01.txt
@@ -432,6 +432,11 @@
method public androidx.ui.core.IntPx getWidth();
}
+ public enum LayoutDirection {
+ enum_constant public static final androidx.ui.core.LayoutDirection Ltr;
+ enum_constant public static final androidx.ui.core.LayoutDirection Rtl;
+ }
+
public enum PointerEventPass {
enum_constant public static final androidx.ui.core.PointerEventPass InitialDown;
enum_constant public static final androidx.ui.core.PointerEventPass PostDown;
diff --git a/ui/ui-core/api/restricted_current.txt b/ui/ui-core/api/restricted_current.txt
index bac3029..0a4e798 100644
--- a/ui/ui-core/api/restricted_current.txt
+++ b/ui/ui-core/api/restricted_current.txt
@@ -432,6 +432,11 @@
method public androidx.ui.core.IntPx getWidth();
}
+ public enum LayoutDirection {
+ enum_constant public static final androidx.ui.core.LayoutDirection Ltr;
+ enum_constant public static final androidx.ui.core.LayoutDirection Rtl;
+ }
+
public enum PointerEventPass {
enum_constant public static final androidx.ui.core.PointerEventPass InitialDown;
enum_constant public static final androidx.ui.core.PointerEventPass PostDown;
diff --git a/ui/ui-core/src/main/java/androidx/ui/core/LayoutDirection.kt b/ui/ui-core/src/main/java/androidx/ui/core/LayoutDirection.kt
new file mode 100644
index 0000000..c826d32
--- /dev/null
+++ b/ui/ui-core/src/main/java/androidx/ui/core/LayoutDirection.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core
+
+/**
+ * A class for defining layout directions.
+ *
+ * A layout direction can be left-to-right (LTR) or right-to-left (RTL).
+ */
+enum class LayoutDirection {
+ /**
+ * Horizontal layout direction is from Left to Right.
+ */
+ Ltr,
+
+ /**
+ * Horizontal layout direction is from Right to Left.
+ */
+ Rtl,
+}
\ No newline at end of file
diff --git a/ui/ui-framework/api/1.0.0-alpha01.txt b/ui/ui-framework/api/1.0.0-alpha01.txt
index cdcff44..1fee6bf 100644
--- a/ui/ui-framework/api/1.0.0-alpha01.txt
+++ b/ui/ui-framework/api/1.0.0-alpha01.txt
@@ -221,6 +221,7 @@
method public static androidx.compose.Ambient<android.content.Context> getContextAmbient();
method public static androidx.compose.Ambient<kotlin.coroutines.CoroutineContext> getCoroutineContextAmbient();
method public static androidx.compose.Ambient<androidx.ui.core.Density> getDensityAmbient();
+ method public static androidx.compose.Ambient<androidx.ui.core.LayoutDirection> getLayoutDirectionAmbient();
method public static androidx.compose.CompositionContext? setContent(android.app.Activity, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.CompositionContext? setContent(android.view.ViewGroup, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @CheckResult(suggest="+") public static <R> androidx.compose.Effect<R> withDensity(kotlin.jvm.functions.Function1<? super androidx.ui.core.DensityReceiver,? extends R> block);
diff --git a/ui/ui-framework/api/current.txt b/ui/ui-framework/api/current.txt
index cdcff44..1fee6bf 100644
--- a/ui/ui-framework/api/current.txt
+++ b/ui/ui-framework/api/current.txt
@@ -221,6 +221,7 @@
method public static androidx.compose.Ambient<android.content.Context> getContextAmbient();
method public static androidx.compose.Ambient<kotlin.coroutines.CoroutineContext> getCoroutineContextAmbient();
method public static androidx.compose.Ambient<androidx.ui.core.Density> getDensityAmbient();
+ method public static androidx.compose.Ambient<androidx.ui.core.LayoutDirection> getLayoutDirectionAmbient();
method public static androidx.compose.CompositionContext? setContent(android.app.Activity, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.CompositionContext? setContent(android.view.ViewGroup, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @CheckResult(suggest="+") public static <R> androidx.compose.Effect<R> withDensity(kotlin.jvm.functions.Function1<? super androidx.ui.core.DensityReceiver,? extends R> block);
diff --git a/ui/ui-framework/api/restricted_1.0.0-alpha01.txt b/ui/ui-framework/api/restricted_1.0.0-alpha01.txt
index cdcff44..1fee6bf 100644
--- a/ui/ui-framework/api/restricted_1.0.0-alpha01.txt
+++ b/ui/ui-framework/api/restricted_1.0.0-alpha01.txt
@@ -221,6 +221,7 @@
method public static androidx.compose.Ambient<android.content.Context> getContextAmbient();
method public static androidx.compose.Ambient<kotlin.coroutines.CoroutineContext> getCoroutineContextAmbient();
method public static androidx.compose.Ambient<androidx.ui.core.Density> getDensityAmbient();
+ method public static androidx.compose.Ambient<androidx.ui.core.LayoutDirection> getLayoutDirectionAmbient();
method public static androidx.compose.CompositionContext? setContent(android.app.Activity, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.CompositionContext? setContent(android.view.ViewGroup, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @CheckResult(suggest="+") public static <R> androidx.compose.Effect<R> withDensity(kotlin.jvm.functions.Function1<? super androidx.ui.core.DensityReceiver,? extends R> block);
diff --git a/ui/ui-framework/api/restricted_current.txt b/ui/ui-framework/api/restricted_current.txt
index cdcff44..1fee6bf 100644
--- a/ui/ui-framework/api/restricted_current.txt
+++ b/ui/ui-framework/api/restricted_current.txt
@@ -221,6 +221,7 @@
method public static androidx.compose.Ambient<android.content.Context> getContextAmbient();
method public static androidx.compose.Ambient<kotlin.coroutines.CoroutineContext> getCoroutineContextAmbient();
method public static androidx.compose.Ambient<androidx.ui.core.Density> getDensityAmbient();
+ method public static androidx.compose.Ambient<androidx.ui.core.LayoutDirection> getLayoutDirectionAmbient();
method public static androidx.compose.CompositionContext? setContent(android.app.Activity, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.CompositionContext? setContent(android.view.ViewGroup, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @CheckResult(suggest="+") public static <R> androidx.compose.Effect<R> withDensity(kotlin.jvm.functions.Function1<? super androidx.ui.core.DensityReceiver,? extends R> block);
diff --git a/ui/ui-framework/src/main/java/androidx/ui/core/Wrapper.kt b/ui/ui-framework/src/main/java/androidx/ui/core/Wrapper.kt
index fd403be..0b7d2f0 100644
--- a/ui/ui-framework/src/main/java/androidx/ui/core/Wrapper.kt
+++ b/ui/ui-framework/src/main/java/androidx/ui/core/Wrapper.kt
@@ -153,6 +153,18 @@
// with nested AndroidCraneView case
val focusManager = +memo { FocusManager() }
val configuration = +state { context.applicationContext.resources.configuration }
+
+ // We don't use the attached View's layout direction here since that layout direction may not
+ // be resolved since the widgets may be composed without attaching to the RootViewImpl.
+ // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
+ // configuration) as a default layout direction.
+ val layoutDirection = when(configuration.value.layoutDirection) {
+ android.util.LayoutDirection.LTR -> LayoutDirection.Ltr
+ android.util.LayoutDirection.RTL -> LayoutDirection.Rtl
+ // API doc says Configuration#getLayoutDirection only returns LTR or RTL.
+ // Fallback to LTR for unexpected return value.
+ else -> LayoutDirection.Ltr
+ }
+memo {
craneView.configurationChangeObserver = {
// onConfigurationChange is the correct hook to update configuration, however it is
@@ -163,6 +175,7 @@
configuration.value = context.applicationContext.resources.configuration
}
}
+
ContextAmbient.Provider(value = context) {
CoroutineContextAmbient.Provider(value = coroutineContext) {
DensityAmbient.Provider(value = Density(context)) {
@@ -172,7 +185,9 @@
AutofillTreeAmbient.Provider(value = craneView.autofillTree) {
AutofillAmbient.Provider(value = craneView.autofill) {
ConfigurationAmbient.Provider(value = configuration.value) {
- content()
+ LayoutDirectionAmbient.Provider(value = layoutDirection) {
+ content()
+ }
}
}
}
@@ -197,6 +212,8 @@
// This will ultimately be replaced by Autofill Semantics (b/138604305).
val AutofillTreeAmbient = Ambient.of<AutofillTree>()
+val LayoutDirectionAmbient = Ambient.of<LayoutDirection>()
+
internal val FocusManagerAmbient = Ambient.of<FocusManager>()
internal val TextInputServiceAmbient = Ambient.of<TextInputService?>()
diff --git a/ui/ui-text/src/main/java/androidx/ui/text/TextDelegate.kt b/ui/ui-text/src/main/java/androidx/ui/text/TextDelegate.kt
index ba418cc..2ae7994 100644
--- a/ui/ui-text/src/main/java/androidx/ui/text/TextDelegate.kt
+++ b/ui/ui-text/src/main/java/androidx/ui/text/TextDelegate.kt
@@ -17,7 +17,6 @@
package androidx.ui.text
import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.annotation.RestrictTo.Scope.LIBRARY
import androidx.annotation.VisibleForTesting
import androidx.ui.core.Constraints
@@ -107,7 +106,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
class TextDelegate(
- text: AnnotatedString? = null,
+ val text: AnnotatedString? = null,
val style: TextStyle? = null,
val paragraphStyle: ParagraphStyle? = null,
val maxLines: Int? = null,
@@ -125,38 +124,28 @@
internal var multiParagraph: MultiParagraph? = null
private set
- @VisibleForTesting
- internal var needsLayout = true
+ private var needsLayout = true
private set
- @VisibleForTesting
- internal var layoutTemplate: Paragraph? = null
+ private var layoutTemplate: Paragraph? = null
private set
private var overflowShader: Shader? = null
- @VisibleForTesting
- internal var hasVisualOverflow = false
+ var hasVisualOverflow = false
private set
private var lastMinWidth: Float = 0.0f
private var lastMaxWidth: Float = 0.0f
- @RestrictTo(LIBRARY_GROUP)
- var text: AnnotatedString? = text
- set(value) {
- if (field == value) return
- field = value
- multiParagraph = null
- needsLayout = true
- }
-
- internal val textStyle: TextStyle
+ private val textStyle: TextStyle
get() = style ?: TextStyle()
+ @VisibleForTesting
internal val textAlign: TextAlign =
if (paragraphStyle?.textAlign != null) paragraphStyle.textAlign else DefaultTextAlign
+ @VisibleForTesting
internal val textDirection: TextDirection? =
paragraphStyle?.textDirection ?: DefaultTextDirection
@@ -168,6 +157,7 @@
)
}
+ @VisibleForTesting
internal fun createParagraphStyle(): ParagraphStyle {
return ParagraphStyle(
textAlign = textAlign,
@@ -478,10 +468,7 @@
/**
* Returns the bottom y coordinate of the given line.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun getLineBottom(lineIndex: Int): Float {
assert(!needsLayout)
return multiParagraph!!.getLineBottom(lineIndex)
@@ -491,10 +478,7 @@
* Returns the line number on which the specified text offset appears.
* If you ask for a position before 0, you get 0; if you ask for a position
* beyond the end of the text, you get the last line.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun getLineForOffset(offset: Int): Int {
assert(!needsLayout)
return multiParagraph!!.getLineForOffset(offset)
@@ -502,10 +486,7 @@
/**
* Get the primary horizontal position for the specified text offset.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun getPrimaryHorizontal(offset: Int): Float {
assert(!needsLayout)
return multiParagraph!!.getPrimaryHorizontal(offset)
@@ -522,10 +503,7 @@
* the top, bottom, left and right of a character.
*
* Valid only after [layout] has been called.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun getBoundingBox(offset: Int): Rect {
assert(!needsLayout)
return multiParagraph!!.getBoundingBox(offset)
diff --git a/ui/ui-text/src/test/java/androidx/ui/text/TextDelegateTest.kt b/ui/ui-text/src/test/java/androidx/ui/text/TextDelegateTest.kt
index 1c2f508..3b34061 100644
--- a/ui/ui-text/src/test/java/androidx/ui/text/TextDelegateTest.kt
+++ b/ui/ui-text/src/test/java/androidx/ui/text/TextDelegateTest.kt
@@ -120,18 +120,6 @@
}
@Test
- fun `text setter`() {
- val textDelegate = TextDelegate(density = density, resourceLoader = resourceLoader)
- val text = AnnotatedString(text = "Hello")
-
- textDelegate.text = text
-
- assertThat(textDelegate.text).isEqualTo(text)
- assertThat(textDelegate.multiParagraph).isNull()
- assertThat(textDelegate.needsLayout).isTrue()
- }
-
- @Test
fun `createParagraphStyle without TextStyle in AnnotatedText`() {
val maxLines = 5
val overflow = TextOverflow.Ellipsis
diff --git a/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java b/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
index 868ce63..83f2d59 100644
--- a/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
+++ b/work/workmanager-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
@@ -20,10 +20,15 @@
import static org.hamcrest.MatcherAssert.assertThat;
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import androidx.lifecycle.Observer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.work.Configuration;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
@@ -41,7 +46,11 @@
import org.junit.runner.RunWith;
import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@@ -50,11 +59,18 @@
private Context mContext;
private TestDriver mTestDriver;
+ private Handler mHandler;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
- WorkManagerTestInitHelper.initializeTestWorkManager(mContext);
+ mHandler = new Handler(Looper.getMainLooper());
+ // Don't set the task executor
+ Configuration configuration = new Configuration.Builder()
+ .setExecutor(new SynchronousExecutor())
+ .setMinimumLoggingLevel(Log.DEBUG)
+ .build();
+ WorkManagerTestInitHelper.initializeTestWorkManager(mContext, configuration);
mTestDriver = WorkManagerTestInitHelper.getTestDriver(mContext);
CountingTestWorker.COUNT.set(0);
}
@@ -186,6 +202,106 @@
mTestDriver.setPeriodDelayMet(request.getId());
}
+ @Test
+ @LargeTest
+ public void testWorker_multipleSetInitialDelayMet_noDeadLock()
+ throws InterruptedException, ExecutionException {
+
+ Configuration configuration = new Configuration.Builder()
+ .setMinimumLoggingLevel(Log.DEBUG)
+ .build();
+ WorkManagerTestInitHelper.initializeTestWorkManager(mContext, configuration);
+ mTestDriver = WorkManagerTestInitHelper.getTestDriver(mContext);
+
+ // This should not deadlock
+ final OneTimeWorkRequest request = createWorkRequest();
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+ mTestDriver.setInitialDelayMet(request.getId());
+ mTestDriver.setInitialDelayMet(request.getId());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ // Use the main looper to observe LiveData because we are using a SerialExecutor which is
+ // wrapping a SynchronousExecutor.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ workManager.getWorkInfoByIdLiveData(request.getId()).observeForever(
+ new Observer<WorkInfo>() {
+ @Override
+ public void onChanged(WorkInfo workInfo) {
+ if (workInfo != null && workInfo.getState().isFinished()) {
+ latch.countDown();
+ }
+ }
+ });
+ }
+ });
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertThat(latch.getCount(), is(0L));
+ }
+
+ @Test
+ @LargeTest
+ public void testWorker_multipleSetInitialDelayMetMultiThreaded_noDeadLock()
+ throws InterruptedException {
+
+ Configuration configuration = new Configuration.Builder()
+ .setMinimumLoggingLevel(Log.DEBUG)
+ .build();
+ WorkManagerTestInitHelper.initializeTestWorkManager(mContext, configuration);
+ mTestDriver = WorkManagerTestInitHelper.getTestDriver(mContext);
+
+ // This should not deadlock
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ int numberOfWorkers = 10;
+ final ExecutorService service = Executors.newFixedThreadPool(numberOfWorkers);
+ for (int i = 0; i < numberOfWorkers; i++) {
+ service.submit(new Runnable() {
+ @Override
+ public void run() {
+ final OneTimeWorkRequest request = createWorkRequest();
+ workManager.enqueue(request);
+ mTestDriver.setInitialDelayMet(request.getId());
+ mTestDriver.setInitialDelayMet(request.getId());
+ }
+ });
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ // Use the main looper to observe LiveData because we are using a SerialExecutor which is
+ // wrapping a SynchronousExecutor.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Using the implicit tag name.
+ workManager.getWorkInfosByTagLiveData(TestWorker.class.getName()).observeForever(
+ new Observer<List<WorkInfo>>() {
+ @Override
+ public void onChanged(List<WorkInfo> workInfos) {
+ boolean completed = true;
+ if (workInfos != null && !workInfos.isEmpty()) {
+ for (WorkInfo workInfo : workInfos) {
+ if (!workInfo.getState().isFinished()) {
+ completed = false;
+ break;
+ }
+ }
+ }
+ if (completed) {
+ latch.countDown();
+ }
+ }
+ });
+ }
+ });
+
+ latch.await(10, TimeUnit.SECONDS);
+ service.shutdownNow();
+ assertThat(latch.getCount(), is(0L));
+ }
+
private static OneTimeWorkRequest createWorkRequest() {
return new OneTimeWorkRequest.Builder(TestWorker.class).build();
}
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/TestScheduler.java b/work/workmanager-testing/src/main/java/androidx/work/testing/TestScheduler.java
index afc4f41..1595350 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/TestScheduler.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/TestScheduler.java
@@ -50,8 +50,6 @@
private final Map<String, InternalWorkState> mPendingWorkStates;
private final Map<String, InternalWorkState> mTerminatedWorkStates;
- private static final Object sLock = new Object();
-
TestScheduler(@NonNull Context context) {
mContext = context;
mPendingWorkStates = new HashMap<>();
@@ -64,28 +62,24 @@
return;
}
- synchronized (sLock) {
- List<String> workSpecIdsToSchedule = new ArrayList<>(workSpecs.length);
- for (WorkSpec workSpec : workSpecs) {
- if (!mPendingWorkStates.containsKey(workSpec.id)) {
- mPendingWorkStates.put(workSpec.id, new InternalWorkState(mContext, workSpec));
- }
- workSpecIdsToSchedule.add(workSpec.id);
+ List<String> workSpecIdsToSchedule = new ArrayList<>(workSpecs.length);
+ for (WorkSpec workSpec : workSpecs) {
+ if (!mPendingWorkStates.containsKey(workSpec.id)) {
+ mPendingWorkStates.put(workSpec.id, new InternalWorkState(mContext, workSpec));
}
- scheduleInternal(workSpecIdsToSchedule);
+ workSpecIdsToSchedule.add(workSpec.id);
}
+ scheduleInternal(workSpecIdsToSchedule);
}
@Override
public void cancel(@NonNull String workSpecId) {
- synchronized (sLock) {
- WorkManagerImpl.getInstance(mContext).stopWork(workSpecId);
- mPendingWorkStates.remove(workSpecId);
- // We don't need to keep track of cancelled workSpecs. This is because subsequent calls
- // to enqueue() will no-op because insertWorkSpec in WorkDatabase has a conflict
- // policy of @Ignore. So TestScheduler will _never_ be asked to schedule those
- // WorkSpecs.
- }
+ // We don't need to keep track of cancelled workSpecs. This is because subsequent calls
+ // to enqueue() will no-op because insertWorkSpec in WorkDatabase has a conflict
+ // policy of @Ignore. So TestScheduler will _never_ be asked to schedule those
+ // WorkSpecs.
+ WorkManagerImpl.getInstance(mContext).stopWork(workSpecId);
+ mPendingWorkStates.remove(workSpecId);
}
/**
@@ -96,17 +90,15 @@
* @throws IllegalArgumentException if {@code workSpecId} is not enqueued
*/
void setAllConstraintsMet(@NonNull UUID workSpecId) {
- synchronized (sLock) {
- String id = workSpecId.toString();
- if (!mTerminatedWorkStates.containsKey(id)) {
- InternalWorkState internalWorkState = mPendingWorkStates.get(id);
- if (internalWorkState == null) {
- throw new IllegalArgumentException(
- "Work with id " + workSpecId + " is not enqueued!");
- }
- internalWorkState.mConstraintsMet = true;
- scheduleInternal(Collections.singletonList(workSpecId.toString()));
+ String id = workSpecId.toString();
+ if (!mTerminatedWorkStates.containsKey(id)) {
+ InternalWorkState internalWorkState = mPendingWorkStates.get(id);
+ if (internalWorkState == null) {
+ throw new IllegalArgumentException(
+ "Work with id " + workSpecId + " is not enqueued!");
}
+ internalWorkState.mConstraintsMet = true;
+ scheduleInternal(Collections.singletonList(workSpecId.toString()));
}
}
@@ -118,17 +110,15 @@
* @throws IllegalArgumentException if {@code workSpecId} is not enqueued
*/
void setInitialDelayMet(@NonNull UUID workSpecId) {
- synchronized (sLock) {
- String id = workSpecId.toString();
- if (!mTerminatedWorkStates.containsKey(id)) {
- InternalWorkState internalWorkState = mPendingWorkStates.get(id);
- if (internalWorkState == null) {
- throw new IllegalArgumentException(
- "Work with id " + workSpecId + " is not enqueued!");
- }
- internalWorkState.mInitialDelayMet = true;
- scheduleInternal(Collections.singletonList(workSpecId.toString()));
+ String id = workSpecId.toString();
+ if (!mTerminatedWorkStates.containsKey(id)) {
+ InternalWorkState internalWorkState = mPendingWorkStates.get(id);
+ if (internalWorkState == null) {
+ throw new IllegalArgumentException(
+ "Work with id " + workSpecId + " is not enqueued!");
}
+ internalWorkState.mInitialDelayMet = true;
+ scheduleInternal(Collections.singletonList(workSpecId.toString()));
}
}
@@ -140,29 +130,25 @@
* @throws IllegalArgumentException if {@code workSpecId} is not enqueued
*/
void setPeriodDelayMet(@NonNull UUID workSpecId) {
- synchronized (sLock) {
- String id = workSpecId.toString();
- InternalWorkState internalWorkState = mPendingWorkStates.get(id);
- if (internalWorkState == null) {
- throw new IllegalArgumentException(
- "Work with id " + workSpecId + " is not enqueued!");
- }
- internalWorkState.mPeriodDelayMet = true;
- scheduleInternal(Collections.singletonList(workSpecId.toString()));
+ String id = workSpecId.toString();
+ InternalWorkState internalWorkState = mPendingWorkStates.get(id);
+ if (internalWorkState == null) {
+ throw new IllegalArgumentException(
+ "Work with id " + workSpecId + " is not enqueued!");
}
+ internalWorkState.mPeriodDelayMet = true;
+ scheduleInternal(Collections.singletonList(workSpecId.toString()));
}
@Override
public void onExecuted(@NonNull String workSpecId, boolean needsReschedule) {
- synchronized (sLock) {
- InternalWorkState internalWorkState = mPendingWorkStates.get(workSpecId);
- if (internalWorkState != null) {
- if (internalWorkState.mWorkSpec.isPeriodic()) {
- internalWorkState.reset();
- } else {
- mTerminatedWorkStates.put(workSpecId, internalWorkState);
- mPendingWorkStates.remove(workSpecId);
- }
+ InternalWorkState internalWorkState = mPendingWorkStates.get(workSpecId);
+ if (internalWorkState != null) {
+ if (internalWorkState.mWorkSpec.isPeriodic()) {
+ internalWorkState.reset();
+ } else {
+ mTerminatedWorkStates.put(workSpecId, internalWorkState);
+ mPendingWorkStates.remove(workSpecId);
}
}
}
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index 4e7c916..bb84e45 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -49,6 +49,14 @@
// Note: This implies that the call to ForceStopRunnable() actually does nothing.
// This is okay when testing.
+
+ // IMPORTANT: Leave the main thread executor as a Direct executor. This is very important.
+ // Otherwise we subtly change the order of callbacks. onExecuted() will execute after
+ // a call to StopWorkRunnable(). StopWorkRunnable() removes the pending WorkSpec and
+ // therefore the call to onExecuted() does not add the workSpecId to the list of
+ // terminated WorkSpecs. This is because internalWorkState == null.
+ // Also for PeriodicWorkRequests, Schedulers.schedule() will run before the call to
+ // onExecuted() and therefore PeriodicWorkRequests will always run twice.
super(
context,
configuration,