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,